ぼく 「やってみるか」
List<String> list = new ArrayList<>(Arrays.asList("foo", "hoo", "hoge"));
for (String str : list) {
list.add(str);
}
System.out.println(list);
// 例外発生
// 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)
ぼく 「ConcurrentModificationException?」
ぼく 「お前は誰だ」
ぼく 「forEachでやってみるか、」
list.stream().forEach(list::add);
System.out.println(list);
// Exception in thread "main" java.util.ConcurrentModificationException
// at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1660)
// at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:658)
ぼく 「だめだ、、」
ぼく 「こんなんじゃ、ただでさえ眠いお昼あとの時間を__シエスタ__にしてしまうじゃないか、、」
ぼく 「ああ、、」
チームリーダー 「sigくん」
チームリーダー 「午前中に頼んでた調査の件だけど、何かわかった?」
ぼく 「リーダー、、!」
ぼく 「(やってないです)」
ぼく 「(とりあえず__なにかしらの作業やってます感__だすために、今の画面見せたろ)」
ぼく 「こんな感じです」
チームリーダー 「ほう、、」
チームリーダー 「このConcurrentModificationExceptionっていう例外、【繰り返し処理中に、繰り返し元のリストの要素数に変更があった時】に発生するんだよね」
チームリーダー 「Javaのイテレータは__自身の構造を変更した回数__を内部で__modCount__という変数名で記録していて、その変更回数に繰り返し処理中ズレが見つかると、例外を投げるようになっているんだ」
// addでmodCount(変更回数)が更新される
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
// modCount(変更回数)のチェック => ズレがあったら例外を投げる
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
// 次のループに向かう度に変更回数のチェックを行う
public E next() {
checkForComodification(); // modCount(変更回数)チェックが実行される
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];
}
ぼく 「おお」
ぼく 「なんでわざわざこんなことを」
チームリーダー 「Javaの多くのコレクションは__フェイルファスト__を採用してるからな」
ぼく 「フェイルファスト?」
チームリーダー 「転ばぬ先の杖だ。失敗するより先に、__危険を察知した時点で例外を投げている__んだよ」
チームリーダー 「Javaは基本的にマルチスレッドでのコレクションの構造の書き換えを__保証しない__って言っているからな」
チームリーダー 「今回はシングルスレッドだったが、万が一マルチスレッドでの動作だったとしても大丈夫なように、とりあえず先に例外を投げるようにしているんだ」
ぼく 「なるほど」
ぼく 「用心深いやつですね、、」
チームリーダー 「ちなみに、イテレータの外部から構造を変更するとアウトだが、イテレータの内部からならいける」
List<String> list = new ArrayList<>(Arrays.asList("foo", "hoo", "hoge"));
for (ListIterator<String> itr = list.listIterator(); itr.hasNext();) {
String str = itr.next();
itr.add(str + "_itr");
}
System.out.println(list);
// [foo, foo_itr, hoo, hoo_itr, hoge, hoge_itr]
ぼく 「リーダー、無限ループしていないのですが」
ぼく 「(定時まで無限に増え続ける配列を眺めて遊びたいのに、、)」
チームリーダー 「ListIterator.addで追加した要素はイテレータの__nextの直前__に挿し込まれるからな」
チームリーダー 「以降のhasNext()やnext()に影響を及ぼさない」
ぼく 「(まじか)」
チームリーダー 「というかそろそろ言いたいんだが」
チームリーダー 「これ__仕事と関係ないよな__」
チームリーダー 「お願いしてた調査、やってくれたんだよな?」
ぼく 「(やってません)」
チームリーダー 「よくわかった」
チームリーダー 「仕事なら無限にあるからな、とりあえず暇そうなsigくんには今週中にあれとこれと、、」
ぼく 「リーダー!」
チームリーダー 「なんだ」
ぼく 「危険を察知したので早退します」
ぼく 「フェイルファスト帰宅」
チームリーダー 「いいから仕事しろ」
ざっくりとしたまとめ: ConcurrentModificationExceptionとは
- 反復処理中に、反復元のコレクションの構造に変化があると投げられる例外
- Javaのコレクションは反復処理中のリストの構造書き換えを保証していないので、__これを良しとしないコレクションは__フェイルファストでこの例外を投げる(保証しないけどまあいいよね、と許可するクラスもある)
- 「臆病だからシングルスレッドだろうがなんだろうが、とりあえず先に例外投げとくよ!」的スタンス