【Java】JavaのIteratorの仕組みと拡張for文との関係を解説

Java

こんにちは、かつコーチです。
「拡張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, 中級者向け, コレクション

タイトルとURLをコピーしました