ConcurrentModificationException の原因と直し方
ループ中のリスト変更で発生します。removeIf、Iterator、逆順ループの3つの解法を比較します。
エラーメッセージの読み方
Exception in thread "main" java.util.ConcurrentModificationException
main- スレッド名 — mainなら起動直後の処理で発生しています
java.util- パッケージ — java.lang なら標準の基本例外です
ConcurrentModificationException- 例外クラス — 何が起きたか。ここを検索するのが最短です
このエラーが出る典型パターン
パターン1
1 import java.util.ArrayList; 2 import java.util.List; 3 4 public class Main { 5 public static void main(String[] args) { 6 List<String> list = new ArrayList<>(List.of("a", "b", "c", "d")); 7 for (String s : list) if (s.equals("b")) list.remove(s); ^ 8 System.out.println(list); 9 } 10 }
拡張forで回している最中にリストを変更すると例外になります。removeIfかIteratorを使います。
直し方: for (String s : list) if (s.equals("b")) list.remove(s); を list.removeIf(s -> s.equals("b")); にします。
パターン2
1 import java.util.ArrayList; 2 import java.util.List; 3 4 public class Main { 5 public static void main(String[] args) { 6 List<Integer> list = new ArrayList<>(List.of(1, 2, 3)); 7 for (int i = list.size() ; i >= 0; i--) { ^ 8 if (list.get(i) == 2) list.remove(i); 9 } 10 System.out.println(list); 11 } 12 }
添字で削除するときは後ろから回します。前から削ると以降の要素が繰り上がって位置がずれます。
直し方: list.size() を list.size() - 1 にします。
パターン3
1 import java.util.ArrayList; 2 import java.util.List; 3 4 public class Main { 5 public static void main(String[] args) { 6 List<String> list = List.of("a") ; ^ 7 list.add("x"); 8 System.out.println(list); 9 } 10 }
List.ofは変更不可のリストを返します。追加や削除をするならArrayListに包み直します。
直し方: List.of("a") を new ArrayList<>(List.of("a")) にします。
パターン4
1 import java.util.ArrayList; 2 import java.util.Iterator; 3 import java.util.List; 4 5 public class Main { 6 public static void main(String[] args) { 7 List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4)); 8 Iterator<Integer> it = list.iterator(); 9 while (it.hasNext()) { 10 int n = it.next(); 11 if (n % 2 == 0) list.remove(Integer.valueOf(n)); ^ 12 } 13 System.out.println(list); 14 } 15 }
リスト自体を変更するとConcurrentModificationExceptionです。イテレータ経由で消すit.remove()を使います。
直し方: list.remove(Integer.valueOf(n)) を it.remove() にします。
パターン5
1 import java.util.HashMap; 2 import java.util.Map; 3 4 public class Main { 5 public static void main(String[] args) { 6 Map<String, Integer> m = new HashMap<>(); 7 m.put("a", 1); 8 m.put("b", 2); 9 for (String k : m.keySet()) { 10 m.put(k + k, 0); ^ 11 } 12 System.out.println(m); 13 } 14 }
keySet()を回している最中にMapへput(キーの追加)をすると例外になります。ArrayListと同じ制約がMapにもあります。
直し方: m.put(k + k, 0); を (空) にします。
よくある誤解
例外が出ないこともあります。最後から2番目の要素を削除すると検出をすり抜けるため、たまたま動いてしまいます。
実務での勘所
この例外は、リストの内部にあるmodCountという変更回数カウンタをIteratorが毎回チェックする「fail-fast」という仕組みで検出されています。Javadocにも明記されていますが、これは正確性を保証する仕組みではなくベストエフォート(検出できる場合とできない場合がある)の防御機構です。拡張for文(for (String s : list))は内部的にIteratorを使うただの糖衣構文なので、見た目が普通のループでも同じ理由で同じ例外が起きます。「forを使っているから安全」ではありません。