■ はじめに
現場で拡張For文を使っているリストを操作している処理の中にremove入れたかったのですが、
調べてみるとどうやらコケるらしいので、簡易的な例文で挙動を確認してみました。
備忘録のため誤字脱字等意識せず書いてます。
あらかじめご了承くださいまし。。
■ 例文
今回String型を格納するリストを用意しました。
A、B、Cの文字列を格納しており、拡張文の中でAの文字列が格納されているリストは削除する処理を書いています。
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
for (String s : list) {
if ("A".equals(s)) {
list.remove(s);
}
}
}
・①を実行するとExceptionが発生して処理がこけてしまいました。
コンソールに出力された内容は下記になります。
これを見たところ、最後の行で「Iterator」の文字列が確認できます。
Iteratorとはなんぞや状態だったので調べてみたところ、ListとかMapとかのコレクション要素に繰り返しアクセスする際に使われるIterableインタフェースのメソッドとのこと。
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:967)
at Iterator.main(Iterator.java:13)
・どういう流れかさっぱりなので、Exceptionまでの流れをデバッグで確認してみました。
最初に、①の拡張For文を書いているところでデバックしてみると、下記ソースが実行されていました。
拡張For文の処理開始なのでIteratorの作成をしているようです。
この処理で変数listが、Iteratorとなって返ってきます。
public Iterator<E> iterator() {
return new Itr();
}
/*
* iterator()戻り値
* cursor :0 // 次に返す要素のインデックス
* expectedModCount:3 // イテレータによりコレクションが変更された回数
* lastRet :-1 //最後の要素 なければ-1を返す
*/
・Iterator作成後拡張For文に戻りますが、ステップインをすると以下ソースが実行されていました。
hasNextメソッドは、反復にさらに要素がある場合は true を返すメソッドとのこと。
ここでは、アクセスしている要素のインデックス(cursor)が要素数(size)と一致していないことを確認していることが確認できます。
今回実行しているソースでは、インデックス0にアクセスしていて、要素数は3つなため「true」が返ってきます。
public boolean hasNext() {
return cursor != size;
}
/*
* hasNext()戻り値
* true
*/
・hasNextの処理後は再度拡張For文に戻ってきます。
ステップインを行うと次はnextメソッドに処置が移ります。
nextメソッドは次の要素を取り出すメソッドとのこと。
そのため、今回は"A"が戻り値として返ってきます。
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];
}
/*
* next()戻り値
* "A"
*/
・nextメソッドで"A"の戻り値を受け取った後、①のequalメソッドが実行されtrueが返ってきます。
次の処理removeメソッドに移ります。
ステップインすると以下ソースが実行されていました。
⑦のfastRemoveメソッドで"A"が格納されているリストが削除されています。
public boolean remove(Object o) {
final Object[] es = elementData;
final int size = this.size;
int i = 0;
found: {
if (o == null) {
for (; i < size; i++)
if (es[i] == null)
break found;
} else {
for (; i < size; i++)
if (o.equals(es[i]))
break found;
}
return false;
}
fastRemove(es, i);
return true;
}
private void fastRemove(Object[] es, int i) {
// コレクション操作を行うため、modContの値を更新
modCount++; // 3 → 4
final int newSize;
if ((newSize = size - 1) > i)
System.arraycopy(es, i + 1, es, i, newSize - i);
es[size = newSize] = null;
}
・"A"のリストの削除が完了し、次の"B"のリストの処理が始まります。
先ほど同様、④のhasNextメソッドが実行されます。
まだ最後まで処理は終わっていないので戻り値はtrueです。
そして次は⑤ソースのnextメソッドが実行されます。
ここでcheckForComodificationメソッド内で問題が起きました。
if文の条件分岐でfalseが返ってきてConcurrentModificationExceptionが発生しました。
これは、modCount(コレクションが操作された回数)とexpectedModCount(イテレータによりコレクションが変更された回数)を比較した結果、値が一致していなかったためExceptionとなったようです。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
・では、どうしたら正常に処理が完了できるのか、、、結論Iteratorで用意されているremoveメソッドを使えばexpectedModCountが更新されて正常に処理がされるそう。。
List操作からIterator操作にソースを修正します。
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
// 変数listのIteratorを作成
Iterator<String> itr = list.iterator();
// 次の要素が存在するか判定
while (itr.hasNext()) {
// 次の要素の値を変数strに代入
String str = itr.next();
if ("A".equals(str)) {
// Iteratorクラスのremoveメソッドで削除処理
itr.remove();
}
}
}
・実行してみると削除処理は、hasNextメソッドやnextメソッドを使っていたIteratorクラスを実現したitrクラスのremoveメソッドに切り替わっていました。
fastRemoveメソッドでmodCountが更新された後、removeメソッド内でexpectedModCountに代入しているので差異が発生せず、次回以降Exceptionが発生しないということがわかりました。
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
// ⑦ fastRemoveメソッドが実行され、modCountの値が4に更新される
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
// 変更前: 3 = 4
// 変更後: 4 = 4
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
■ ArraryList.class itrクラス
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
// prevent creating a synthetic constructor
Itr() {}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
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];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
@Override
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int size = ArrayList.this.size;
int i = cursor;
if (i < size) {
final Object[] es = elementData;
if (i >= es.length)
throw new ConcurrentModificationException();
for (; i < size && modCount == expectedModCount; i++)
action.accept(elementAt(es, i));
// update once at end to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
■ 参照