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: ここで述べたすべての手法(Iterator、ListIterator、removeIf)をサポート。
- Set: インデックスはないが、標準の Iterator と removeIf は動作。
- Map: 条件で削除するには entrySet() に対するイテレータを使う:
さらに Java 8+ なら次のようにもっと簡単: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}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. アプローチの比較
復習として小さな表にまとめます。こういう一覧は頭に入りやすいです。
| 方法 | 対応バージョン | 簡潔さ | 安全性 | 柔軟性 |
|---|---|---|---|---|
|
Java 5+ | - | ❌ | - |
|
Java 5+ | + | ✅ | + |
|
Java 5+ | + | ✅ | ++ |
|
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 を使いましょう。
GO TO FULL VERSION