Code Fix

中級

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  }
Exception in thread "main" java.util.ConcurrentModificationExceptionexited with code 1

拡張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  }
Exception in thread "main" java.lang.IndexOutOfBoundsExceptionexited with code 1

添字で削除するときは後ろから回します。前から削ると以降の要素が繰り上がって位置がずれます。

直し方: 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  }
Exception in thread "main" java.lang.UnsupportedOperationExceptionexited with code 1

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  }
Exception in thread "main" java.util.ConcurrentModificationExceptionexited with code 1

リスト自体を変更すると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  }
Exception in thread "main" java.util.ConcurrentModificationExceptionexited with code 1

keySet()を回している最中にMapへput(キーの追加)をすると例外になります。ArrayListと同じ制約がMapにもあります。

直し方: m.put(k + k, 0); を (空) にします。

この問題を解いてみる →

よくある誤解

例外が出ないこともあります。最後から2番目の要素を削除すると検出をすり抜けるため、たまたま動いてしまいます。

実務での勘所

この例外は、リストの内部にあるmodCountという変更回数カウンタをIteratorが毎回チェックする「fail-fast」という仕組みで検出されています。Javadocにも明記されていますが、これは正確性を保証する仕組みではなくベストエフォート(検出できる場合とできない場合がある)の防御機構です。拡張for文(for (String s : list))は内部的にIteratorを使うただの糖衣構文なので、見た目が普通のループでも同じ理由で同じ例外が起きます。「forを使っているから安全」ではありません。

演習をはじめる

関連するエラー

広告
広告スロット(未設定)