公開日:2/28/2021  更新日:3/26/2022

  • twitter
  • facebook
  • line

【Java】拡張for文で発生するエラーについて

事象

以下のコードを実行すると、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)

ConcurrentModificationException とは?

Java 8 SE ConcurrentModificationExceptionドキュメント

クラス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 がスローされていたと理解できる。

まとめ

今回のエラーが起きた原因フロー

  1. "C"が取り出される直前,Iteratorのカーソル は"C"の位置である 2 となる。

  2. next() によって"C" が取り出され,Iterator の現在位置は1 加算され 3、remove()でリストのサイズは2になる hasNext()はサイズ≠位置となりtrue。ループ継続。

  3. 次の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()を使用
	}
}

戻る