イテレーション中にコレクションを変更したときの java.util.ConcurrentModificationException を修正する

intermediate Java2026-03-18| Java 8以降、任意のOS(Windows / Linux / macOS)、java.utilコレクション(ArrayList、HashMap、HashSetなど)を使用する任意のフレームワーク

Error Message

java.util.ConcurrentModificationException
#java#コレクション#並行処理#例外#ConcurrentModificationException

エラーの内容

コレクションをループ処理中に要素を削除(または追加)しようとすると、Javaが強制停止します:

Exception in thread "main" java.util.ConcurrentModificationException
    at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
    at java.base/java.util.ArrayList$Itr.next(ArrayList.java:967)
    at com.example.MyClass.processItems(MyClass.java:24)

ほとんどの場合、for-eachループの中でイテレート中のコレクションに対して直接list.remove()list.add()、またはmap.put()を呼び出しているのが原因です。

根本原因

Javaの主要なコレクション(ArrayListHashMapHashSet)は、modCountという内部カウンターを持っています。このカウンターはコレクションの構造が変更される(追加・削除・クリア)たびにインクリメントされます。

イテレーションを開始すると、イテレーターはそのカウンターのスナップショットを取ります。next()が呼ばれるたびに「開始時からコレクションに変更があったか?」を確認し、変更があった場合は即座に例外をスローします。

これはフェイルファスト設計によるものです。要素が無言でスキップされたり無限ループに陥ったりするよりも、明示的に例外を投げた方が安全だという考え方です。

典型的な例:

List<String> items = new ArrayList<>(List.of("a", "b", "c", "remove-me"));
for (String item : items) {
    if (item.equals("remove-me")) {
        items.remove(item); // ConcurrentModificationExceptionがスローされる
    }
}

修正1:Iteratorのremove()を使う

イテレーター自身にremove()メソッドがあります。これを使うべきです。内部でmodCountを更新するため、チェックが正常に通過します。

List<String> items = new ArrayList<>(List.of("a", "b", "c", "remove-me"));
Iterator<String> iterator = items.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    if (item.equals("remove-me")) {
        iterator.remove(); // 安全に削除できる
    }
}
System.out.println(items); // [a, b, c]

**制限:**削除できるのは現在の要素のみです。イテレーション途中での要素の追加や他の要素の削除はここでは行えません。

修正2:removeIf()を使う(Java 8以降)

単純なフィルタリングにはremoveIf()が最適です。1行で書け、ボイラープレートもありません:

List<String> items = new ArrayList<>(List.of("a", "b", "c", "remove-me"));
items.removeIf(item -> item.equals("remove-me"));
System.out.println(items); // [a, b, c]

ArrayListHashSetLinkedListなど、このメソッドを実装している任意のCollectionで動作します。イテレーターの管理は内部で自動的に処理されます。

修正3:変更内容を収集してループ後に適用する

要素を追加したい場合や、より複雑な処理が必要な場合はどうすれば良いでしょうか?ループ内でコレクションに直接触れないようにしましょう。変更内容を別途蓄積し、イテレーションが完了してから一括適用します。

List<String> items = new ArrayList<>(List.of("a", "b", "c", "remove-me"));
List<String> toRemove = new ArrayList<>();

for (String item : items) {
    if (item.equals("remove-me")) {
        toRemove.add(item);
    }
}

items.removeAll(toRemove);
System.out.println(items); // [a, b, c]

同じ考え方は追加にも使えます。toAddリストに蓄積し、後からitems.addAll(toAdd)を呼び出します:

List<String> toAdd = new ArrayList<>();
for (String item : items) {
    if (item.startsWith("prefix-")) {
        toAdd.add(item + "-processed");
    }
}
items.addAll(toAdd);

修正4:並行アクセスにはCopyOnWriteArrayListを使う

複数のスレッドが同じリストにアクセスする場合は、別の問題になります。java.util.concurrentCopyOnWriteArrayListはまさにこのために作られています:

import java.util.concurrent.CopyOnWriteArrayList;

List<String> items = new CopyOnWriteArrayList<>(List.of("a", "b", "c", "remove-me"));

for (String item : items) {
    if (item.equals("remove-me")) {
        items.remove(item); // 安全 — スナップショットコピーをイテレートするため
    }
}
System.out.println(items); // [a, b, c]

**トレードオフ:**書き込みのたびに内部配列の新しいコピーが作成されます。10,000要素のリストに頻繁に書き込む場合、そのオーバーヘッドはすぐに積み重なります。便宜上ではなく、真に並行アクセスが必要な場合にのみ使用してください。

Mapへの対応

HashMapも同じ条件で同じ例外をスローします。対処法はリストの場合と同様です。entrySet().removeIf()か明示的なイテレーターを使いましょう:

Map<String, Integer> scores = new HashMap<>();
scores.put("alice", 90);
scores.put("bob", 45);
scores.put("charlie", 30);

// スコアが50未満のエントリーを削除
scores.entrySet().removeIf(entry -> entry.getValue() < 50);
System.out.println(scores); // {alice=90}

// イテレーターを使う場合
Iterator<Map.Entry<String, Integer>> it = scores.entrySet().iterator();
while (it.hasNext()) {
    Map.Entry<String, Integer> entry = it.next();
    if (entry.getValue() < 50) {
        it.remove();
    }
}

確認方法

コードを実行して次の3点を確認してください:

  • スタックトレースにConcurrentModificationExceptionが表示されない。
  • ループ後のコレクションに期待通りの要素が含まれている。
  • 出力して確認する:System.out.println(items);

マルチスレッドのコードでは、複数のスレッドから同時にリストに負荷をかけるテストを追加しましょう。簡単なアサーションがスモークテストとして有効です:

// 実行して例外が発生しないことを確認
List<String> result = new ArrayList<>(List.of("keep", "remove", "keep2"));
result.removeIf(s -> s.equals("remove"));
assert result.equals(List.of("keep", "keep2")) : "予期しない結果: " + result;

どの修正方法を選ぶか?

  • 要素の削除のみ:removeIf()——コードが最も少なく、意図が明確。
  • 要素ごとのカスタムロジック:Iterator.remove()
  • **追加や複雑な変更:**変更内容を別のリストに収集し、ループ後に適用する。
  • 真の並行スレッド:CopyOnWriteArrayList——より細かい制御が必要な場合は外部で同期する。

Related Error Notes