公開日:2/28/2021 更新日:3/26/2022
以下のコードを実行すると、java.util.ConcurrentModificationException が発生。
やっていることは単純。リスト作成後、拡張for文でリスト内のCを削除している。
ちなみにだが、Aを削除すると同様に例外が発生するが、Bの要素を削除する場合は例外が発生しない。
リスト内の最後から2番目の要素以外を拡張for文中で削除した場合のみ例外が発生するようだ。
エラー発生コード
List<String> list = new ArrayList<String>();
list.add("A");
list.add("B");
list.add("C");
for (String str : list) {
if ("C".equals(str)) {
list.remove(str);
}
}
コンソール結果
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1042)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:996)
at lesson1.ListSample.main(ListSample.java:14)
Java 8 SE ConcurrentModificationExceptionドキュメント
「あるスレッドのCollectionで反復処理を行っている間に、そのCollectionへの変更を別スレッドから許可しない場合にスローされる例外。」とのこと。また、シングルスレッドでも発生することがあるらしい。 具体例として挙げられている 「たとえば、フェイルファストイテレータを持つコレクションの繰り返し処理を行いながら、スレッドがコレクションを直接修正する場合、イテレータはこの例外をスローします。」 は今回のケースに当てはまりそうだ。
拡張for文はコンパイル時に イテレータ による処理に書き換えられる。
確かにコンソール画面を確認すると、checkForComodification メソッドの箇所で例外が発生していることが分かる。
拡張for文で書かれたコードを下記のように イテレータ を使って書き換えると、やはり同様の例外が発生する。
イテレータで書き換えたコード
List<String> list = new ArrayList<String>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> ite = list.iterator();
while(ite.hasNext()) {
String str = ite.next();
if ("C".equals(str)) {
list.remove(str);
}
}
つまり、例外発生の悪さをしているのは イテレータ の checkForComodification メソッドだ。
checkForComodification は、nextメソッドの初めで毎回実行されている。
next() コード
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
checkForComodification の処理の中身を見てみると、expectedModCount と modCountに差異があればConcurrentModificationExceptionをスローしている。
expectedModCount は、イテレータ によりコレクションが変更された回数。modCount は実際にコレクションが変更された回数。
checkForComodification() コード
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
次に、remove処理を見てみる。remove の内部ではprivateなメソッドfastRemove(Object[] es, int i)を呼び出している。
fastRemove コード
private void fastRemove(Object[] es, int i) {
modCount++;
final int newSize;
if ((newSize = size – 1) > i)
System.arraycopy(es, i + 1, es, i, newSize – i);
es[size = newSize] = null;
}
リストに変更を加えたため modCount に1を追加していることが分かる。
つまり、checkForComodification() では、 expectedModCount = 0 で modCount = 1 となるため ConcurrentModificationException がスローされていたと理解できる。
今回のエラーが起きた原因フロー
"C"が取り出される直前,Iteratorのカーソル は"C"の位置である 2 となる。
next() によって"C" が取り出され,Iterator の現在位置は1 加算され 3、remove()でリストのサイズは2になる hasNext()はサイズ≠位置となりtrue。ループ継続。
次のnext()でリスト側の変更回数がremove()によって1回多いことが判明,例外発生。
イテレータで用意された remove メソッドを使用すれば、expectedModCount = modCount となりエラーが発生しない。
拡張for文のコレクションの反復処理中に、リスト要素への直接修正は避けるべき。
List<String> list = new ArrayList<String>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> ite = list.iterator();
while(ite.hasNext()) {
String str = ite.next();
if ("C".equals(str)) {
//list.remove(str);
ite.remove();//イテレータのremove()を使用
}
}