こんにちは、かつコーチです。
「拡張for文でListを回しながら要素を削除したらエラーになった」という経験はありませんか。
今回は拡張for文の裏側で動いているIteratorの仕組みを理解し、この定番のエラーを解決していきます。
Iteratorとは?
コレクションを1つずつ取り出す仕組み
Iterator(コレクションの要素を1つずつ順番に取り出すための仕組みを提供するインターフェース)は、ListやSet、Mapなど様々なコレクションを同じ方法で走査できるようにするためのものです。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
List<String> names = new ArrayList<>(List.of("かつコーチ", "Aさん", "Bさん"));
Iterator<String> it = names.iterator();
while (it.hasNext()) {
String name = it.next();
System.out.println(name);
}
hasNext()で「次の要素があるか」を確認し、next()で実際に要素を取り出す、というのがIteratorの基本の動きです。
拡張for文との関係
実は、普段よく使う拡張for文(for (String name : names))は、内部でIteratorを使って書き換えられています。
// この拡張for文は…
for (String name : names) {
System.out.println(name);
}
// 内部的には、ほぼ次のコードと同じ意味になる
Iterator<String> it = names.iterator();
while (it.hasNext()) {
String name = it.next();
System.out.println(name);
}
拡張for文は「Iteratorを使った処理を、読みやすく短く書けるようにしたもの」と理解すると、その裏側の動きがイメージしやすくなります。
基本の書き方
Iteratorを直接使う場面
拡張for文で十分な場面が多い一方、要素を走査しながら削除したい場合はIteratorを直接使う必要があります。
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
Iterator<Integer> it = numbers.iterator();
while (it.hasNext()) {
int n = it.next();
if (n % 2 == 0) {
it.remove(); // Iterator経由なら安全に削除できる
}
}
System.out.println(numbers); // [1, 3, 5]
Iteratorが持つremove()メソッドを使うと、走査中でも安全に要素を削除できます。
Mapの走査とIterator
MapのentrySetもIteratorで走査できます。
Map<String, Integer> scores = new HashMap<>();
scores.put("かつコーチ", 90);
scores.put("Aさん", 40);
Iterator<Map.Entry<String, Integer>> it = scores.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, Integer> entry = it.next();
if (entry.getValue() < 60) {
it.remove(); // 60点未満を安全に削除
}
}
System.out.println(scores); // {かつコーチ=90}
よくあるつまずきポイント・エラー対処
拡張for文の中でremoveしてConcurrentModificationExceptionが出た
私が実際にハマった一次情報として、拡張for文の中でlist.remove()を呼び出し、実行時エラーになったことがあります。
// ❌Before:拡張for文の中でListを直接変更する
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
for (int n : numbers) {
if (n % 2 == 0) {
numbers.remove(Integer.valueOf(n)); // 実行時エラー
}
}
このコードを実行すると、次のような例外が発生します。
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1096)
拡張for文は内部でIteratorを使っていますが、走査中にlist.remove()のようにコレクション自体を直接変更すると、Iteratorが「想定外に中身が変わった」と検知してこの例外を投げます。
これは「変更検知(フェイルファスト)」という仕組みによるものです。
// ✅After:Iteratorのremove()を使って安全に削除する
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
Iterator<Integer> it = numbers.iterator();
while (it.hasNext()) {
int n = it.next();
if (n % 2 == 0) {
it.remove(); // Iterator自身に削除を依頼するので安全
}
}
System.out.println(numbers); // [1, 3, 5]
「走査中に要素を削除・追加したくなったら、拡張for文ではなくIteratorに切り替える」というのは、実務でも頻出する重要なルールです。
応用・一歩先の使い方
removeIfで更にシンプルに書く
Java8以降では、単純な条件削除であればremoveIfを使うとIteratorを意識せずに書けます。
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
numbers.removeIf(n -> n % 2 == 0);
System.out.println(numbers); // [1, 3, 5]
内部的には同様にIteratorを使って安全に削除処理をしてくれるため、単純な条件削除であればこちらの方が簡潔です。
自作クラスにIteratorを実装する
Iterableインターフェースを実装すれば、自作クラスも拡張for文で回せるようになります。
class NameList implements Iterable<String> {
private List<String> names = new ArrayList<>(List.of("かつコーチ", "Aさん"));
@Override
public Iterator<String> iterator() {
return names.iterator();
}
}
NameList nameList = new NameList();
for (String name : nameList) {
System.out.println(name); // 自作クラスも拡張for文で回せる
}
「独自のコレクションのようなクラスを作りたい」場合に使う応用テクニックです。
まとめ
この記事のポイント
- Iteratorはコレクションの要素を1つずつ取り出す共通の仕組み
- 拡張for文は内部でIteratorを使って書き換えられている
- 拡張for文の中でコレクションを直接変更すると
ConcurrentModificationExceptionが発生する - 走査しながら削除したいときはIteratorの
remove()かremoveIfを使う Iterableを実装すれば自作クラスも拡張for文で回せる
次に読むべき記事
- Comparator・Comparableで並び替えをカスタマイズする
- Listの基本:ArrayListとLinkedListの違い
タグ: Java, 中級者向け, コレクション
