CodeGym /コース /JAVA 25 SELF /要素の安全な削除

要素の安全な削除

JAVA 25 SELF
レベル 28, レッスン 2
使用可能

1. ConcurrentModificationException の問題

この講義自体は(ほとんど)新しい内容ではありません。しかし、とりわけ本番環境では、ぞんざいなデータ削除は取り返しのつかないバグの代表格であり、とても重要です。そして、きっと気に入るであろう興味深いクラスも登場します。

というわけで、もう一度強調します。削除に for-each は使わない! どれだけ好きでも。

まずは多くの初学者(だけでなくベテランも)を苦しめる古典的な例から始めましょう。

List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));

// すべての偶数を削除してみる
for (Integer n : numbers) {
    if (n % 2 == 0) {
        numbers.remove(n); // ドカン! ConcurrentModificationException
    }
}

一見正しく動きそうですが、実際にはプログラムは例外を投げます。

Exception in thread "main" java.util.ConcurrentModificationException

では何が起きているのか、もう少し詳しく見ていきましょう。for-each(あるいは通常の Iterator)でコレクションを走査する際、コレクション内部には特別な「変更カウンタ」が維持されています。走査中にコレクションがイテレータ自身を介さずに変更されると、このカウンタが「外部からの改変」を検知して例外を投げます。これは、プログラムが破損したデータ構造で動作してしまうのを防ぐための安全装置です。

2. Iterator の使用

走査中に要素を正しく削除するには?

おさらいです。Iterator は、コレクションを走査しつつ、その場で安全に要素を削除できる専用オブジェクトです。たとえるなら、料理を配るだけでなく、テーブルを回りながら皿も片付けられる給仕のようなものです。

イテレータの取得

Iterator<Integer> it = numbers.iterator();

while での走査と it.remove() による削除

リストからすべての偶数を削除する正しい方法は次のとおりです。

List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));

Iterator<Integer> it = numbers.iterator();
while (it.hasNext()) {
    Integer n = it.next();
    if (n % 2 == 0) {
        it.remove(); // 現在の要素を安全に削除
    }
}
System.out.println(numbers); // [1, 3, 5]

重要なポイント: 要素の削除は、イテレータ自身(it.remove())から、かつ it.next() を呼んだ後にのみ可能です。next() なしで remove() を続けて呼ぶと、IllegalStateException を受け取ります。

3. ListIterator: さらに拡張された機能

冒頭で触れた新顔の登場です! ListIterator はリスト(List)用の「強化版」イテレータで、削除だけでなく走査中の追加もでき、前後両方向に移動できます。

通常の Iterator との違い

  • Iterator は単純明快: 前方向のみ、削除のみ。
  • ListIterator は柔軟: 前後に移動でき、削除、add() による追加、さらに set() で現在の要素を置換可能。

例: 削除と追加

List<String> words = new ArrayList<>(List.of("cat", "dog", "bird"));

ListIterator<String> it = words.listIterator();
while (it.hasNext()) {
    String word = it.next();
    if (word.length() == 3) {
        it.remove(); // 3文字の単語を削除
        it.add("pet"); // 直後に削除した位置へ "pet" を追加
    }
}
System.out.println(words); // [pet, pet, bird]

注意: it.add() による追加は、イテレータの現在位置の直後に挿入されます。

4. removeIf による削除

Java 8 から、簡潔で便利なメソッド removeIf が追加されました。これはラムダ式(または任意の Predicate)を受け取り、条件が true を返すすべての要素を削除します。

例: すべての偶数を削除

List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));

numbers.removeIf(n -> n % 2 == 0);

System.out.println(numbers); // [1, 3, 5]

これは短いだけでなく安全です。内部では適切なイテレータを使用するため、ConcurrentModificationException は発生しません。

例: 3文字未満の文字列を削除

List<String> words = new ArrayList<>(List.of("hi", "cat", "no", "elephant"));

words.removeIf(word -> word.length() < 3);

System.out.println(words); // [cat, elephant]

ヒント: 条件に合う要素を削除したいだけなら、removeIf を使いましょう。最も簡潔でモダンな方法です。

5. 実践的な推奨

どの方法を選ぶべき?

  • 条件で要素を削除したい(Java 8+ が使える)場合: removeIf を使う — 短く、分かりやすく、安全。
  • 古い Java を使っている、またはより複雑な走査ロジックが必要な場合: Iterator とそのメソッド remove() を使う。
  • List で、削除だけでなく走査中に追加もしたい場合: ListIterator を使う。

コレクション種別ごとの注意点

  • List: ここで述べたすべての手法(IteratorListIteratorremoveIf)をサポート。
  • Set: インデックスはないが、標準の IteratorremoveIf は動作。
  • Map: 条件で削除するには entrySet() に対するイテレータを使う:
    Map<String, Integer> map = new HashMap<>(Map.of("a", 1, "b", 2, "c", 3));
    Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();
    while (it.hasNext()) {
        Map.Entry<String, Integer> entry = it.next();
        if (entry.getValue() % 2 == 0) {
            it.remove();
        }
    }
    System.out.println(map); // {a=1, c=3}
    
    さらに Java 8+ なら次のようにもっと簡単:
    map.entrySet().removeIf(entry -> entry.getValue() % 2 == 0);
    

6. 実例: ユーザーのフィルタリング

ユーザーのリストがあり、18歳未満のユーザーをすべて削除したいとします。

class User {
    String name;
    int age;
    User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

List<User> users = new ArrayList<>(List.of(
    new User("Anya", 17),
    new User("Boris", 20),
    new User("Vika", 15),
    new User("Gleb", 25)
));

// removeIf で未成年を削除
users.removeIf(user -> user.age < 18);

System.out.println(users); // [Boris (20), Gleb (25)]

7. アプローチの比較

復習として小さな表にまとめます。こういう一覧は頭に入りやすいです。

方法 対応バージョン 簡潔さ 安全性 柔軟性
for-each + remove()
Java 5+ - -
Iterator + remove()
Java 5+ + +
ListIterator
Java 5+ + ++
removeIf
Java 8+ ++ +

8. コレクションからの削除でよくある間違い

間違い1: for-each で削除しようとする

for (String s : list) {
    if (s.equals("test")) {
        list.remove(s); 
    }
}

既に学んだとおり、これはダメです — ConcurrentModificationException になります! イテレータか removeIf を使いましょう。

間違い2: next() の前にイテレータの remove() を呼ぶ

Iterator<String> it = list.iterator();
it.remove(); // IllegalStateException — next() の前には削除できない

間違い3: 変更不可のコレクションから削除しようとする

List<String> immutable = List.of("a", "b", "c");
immutable.removeIf(s -> s.equals("a")); // UnsupportedOperationException

不変コレクションでは削除系メソッドはサポートされていません。

間違い4: イテレータを使わずに values()keySet() から Map の要素を削除しようとする

for (String key : map.keySet()) {
    if (key.startsWith("a")) {
        map.remove(key); // ConcurrentModificationException!
    }
}

entrySet() のイテレータ、または removeIf を使いましょう。

1
タスク
JAVA 25 SELF, レベル 28, レッスン 2
ロック未解除
ペットショップ "Drug" でのペットの再分類 🐶
ペットショップ "Drug" でのペットの再分類 🐶
1
タスク
JAVA 25 SELF, レベル 28, レッスン 2
ロック未解除
コンテスト参加者の結果から「疑わしい」スコアを除去する 🏅
コンテスト参加者の結果から「疑わしい」スコアを除去する 🏅
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION