Engineering
Glossvation Engineering
アルゴリズム

Iterator パターン — Java では `Iterable` 実装 + 拡張 for で終わり

GoF の Iterator パターンを Java で書くときの最小実装。拡張 for 文、Spliterator、Stream との関係まで整理する。

Glossvation ·

Iterator パターン概念図

どういう問題を解くパターンか

コレクションの内部構造を隠したまま、順次アクセスの手段だけを公開する パターン。

  • 呼び出し側は 「配列なのか連結リストなのかツリーなのか」 を知らずに hasNext/next だけで扱える
  • コレクション側は内部データ構造を自由に差し替えられる

Java では既に言語機能とライブラリに取り込まれています。 java.util.Iterator, Iterable, 拡張 for 文, Spliterator, Stream。 したがって Java 開発者がこのパターンを 「自作する」 必要はほぼありません。

このパターンを学ぶ価値は、Java のコレクション API がどう設計されているかを理解する ことにあります。

いつ使う / 使わない

使うべき使わないほうがよい
自作コレクションを拡張 for で使いたい標準 Collection を使っている
内部構造を外に見せたくないList#get(i) で十分
要素を 「遅延評価」 で供給したい全件が既にメモリ上にある

最小実装: 自作コレクションに Iterator を持たせる

データ (ドメインモデル)

public record Person(String name, int age) {}

自作コレクション (Iterable を実装)

public final class PersonList implements Iterable<Person> {
    private Person[] persons;
    private int size = 0;

    public PersonList(int capacity) {
        this.persons = new Person[capacity];
    }

    public void add(Person p) {
        if (size == persons.length) {
            persons = Arrays.copyOf(persons, persons.length * 2);
        }
        persons[size++] = p;
    }

    public int size() { return size; }

    @Override
    public Iterator<Person> iterator() {
        return new Iterator<>() {
            private int index = 0;

            @Override public boolean hasNext() {
                return index < size;
            }

            @Override public Person next() {
                if (!hasNext()) throw new NoSuchElementException();
                return persons[index++];
            }
        };
    }
}

呼び出し側: 3 通り

PersonList list = new PersonList(4);
list.add(new Person("Alice", 30));
list.add(new Person("Bob", 25));

// 1. 拡張 for 文 (もっとも読みやすい)
for (Person p : list) {
    System.out.println(p.name());
}

// 2. Iterator を直接
Iterator<Person> it = list.iterator();
while (it.hasNext()) {
    System.out.println(it.next().name());
}

// 3. Stream (list.spliterator() 経由)
StreamSupport.stream(list.spliterator(), false)
    .filter(p -> p.age() >= 20)
    .map(Person::name)
    .forEach(System.out::println);

ポイント: Iterable<T> さえ実装すれば、拡張 for も Stream も使えるようになります。 「GoF Iterator」 を自分で書く意義は、この最小限の実装の中にあります 。

Iterator と Spliterator と Stream の関係

用途並列
Iterator<T>1 要素ずつ順次アクセス❌
Spliterator<T>分割 + 順次 (並列化のための分割アルゴリズム付き)✅
Stream<T>高レベルな関数型パイプライン✅ (parallelStream)

Iterable を実装すると spliterator() のデフォルト実装が内部で iterator() を使って Spliterator を作ってくれるので、Stream もすぐ使えます。 ただし デフォルト Spliterator は並列化効率が低い ので、並列処理を本気で使うなら Spliterator を自前実装することを検討。

遅延評価 Iterator の例

有限とは限らない (無限列) や巨大すぎてメモリに乗らないデータは、 Iterator で 「必要になった時点で生成」 する実装が自然です。

public final class NaturalNumbers implements Iterable<Integer> {
    private final int limit;

    public NaturalNumbers(int limit) {
        this.limit = limit;
    }

    @Override
    public Iterator<Integer> iterator() {
        return new Iterator<>() {
            private int n = 0;
            @Override public boolean hasNext() { return n < limit; }
            @Override public Integer next() { return ++n; }
        };
    }
}

Stream で同じことをすると Stream.iterate(1, n -> n + 1).limit(limit) と書けます。 書き味は Stream の方が上。Iterator の自作は、Stream API が使えない制約下か、 極端にホットなループで Stream のオーバーヘッドを嫌う場面に限ります。

落とし穴

1. イテレーション中にコレクションを変更する

多くの Iterator 実装は 「fail-fast」 です。 反復中に元コレクションを add/remove すると ConcurrentModificationException。

安全に削除したいなら Iterator 自身の remove() を使う:

Iterator<Person> it = list.iterator();
while (it.hasNext()) {
    if (it.next().age() < 18) {
        it.remove(); // OK (この Iterator が削除を主導している)
    }
}

または removeIf(Predicate) を使う方が読みやすい。

2. 複数スレッドから同じ Iterator を触る

Iterator はスレッド安全ではありません。 並行環境では CopyOnWriteArrayList の弱整合 Iterator を使うか、 Stream の parallelStream に委ねる。

3. 無限列 Iterator を終端操作なしで Stream に食わせる

IntStream.iterate(1, n -> n + 1)  // 無限
    .forEach(System.out::println); // 終わらない

必ず .limit(n) や .takeWhile(...) で有限化。


まとめ: GoF Iterator は 「既に Java の言語機能に組み込まれているパターン」 の典型。 自作するなら Iterable<T> を実装するだけで拡張 for / Stream まで一式使えるようになります。 「内部構造を隠して反復だけ公開する」 という思想は現代でも通用し、 API 設計の基礎として理解しておく価値があります。

関連記事