了解しました。以下のコードには、for-eachループを使用してリストをイテレートする際の挙動とremove()メソッドを使用した時のリストの状態の変化を説明するコメントを追加しています。
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("A"); // リストに"A"を追加
list.add("B"); // リストに"B"を追加
list.add("C"); // リストに"C"を追加
// この時点でリストは ["A", "B", "C"] の状態
for (String str : list) { // リストの各要素をイテレートする
if ("B".equals(str)) { // もし要素が"B"ならば
list.remove(str); // "B"をリストから削除する
// この時点でリストは ["A", "C"] となるが、
// "B" の次の要素 "C" に進むべきところ、
// "B" が削除されたために "C" はスキップされる
} else { // もし要素が"B"でなければ
System.out.println(str); // 要素を出力する
// "A" はこの条件に当てはまるので出力される
}
}
// ループの終わり。"A" のみが出力される。
}
}
このコードを実行すると、以下の出力が得られます:
A
for-eachループは内部的にイテレータを使用しており、要素が削除されると内部的なカウンタが狂うことがありますが、このケースでは "B" が削除された後にループが終了するため、ConcurrentModificationExceptionは発生せず、"C"は単にスキップされるだけです。
追記
"B"が削除された後に"C"がスキップされる理由は、ArrayListの内部動作に基づいています。ArrayListはインデックスベースのコレクションであり、各要素は連続したインデックスに格納されます。要素が削除されると、その要素の後にあるすべての要素が一つ前(つまり、現在のインデックスから見て低いインデックス方向)にシフトされます。
ここで重要なのは、for-each ループが Iteratorを内部で使用してコレクションを走査するということです。Iteratorはコレクションの要素にアクセスするためのカーソルのようなもので、次にアクセスする要素のインデックスを保持しています。
このプログラムでは、次のようなプロセスが発生します:
- "A"が最初に処理され、出力されます(インデックス0)。
- 次に"B"(インデックス1)が処理されますが、
"B".equals(str)がtrueと評価されるため、list.remove(str)が呼び出されて "B" がリストから削除されます。 - "B" を削除すると、"C"(元々はインデックス2にありました)がインデックス1に移動します。
-
for-eachループの内部で使用されているIteratorは、次にインデックス2の要素にアクセスしようとしますが、"B"が削除されたことにより、"C"はもはやインデックス2には存在せず、代わりにインデックス1にあります。 - この結果、
Iteratorは"C"をスキップして、リストの終わりに達してしまいます。
これが"C"がスキップされる理由です。リストの要素を走査しながらその要素を削除する場合、このような振る舞いが起こりうるため、特にfor-each ループの中で remove() を直接呼び出す際には注意が必要です。上述したイテレータやインデックスを直接操作する手法を使用することで、この問題を回避できます。
Top comments (0)