java-recipes

ホーム デザインパターン › DP-16

DP-16: Iterator パターン

コレクションの内部構造(配列か連結リストかなど)を隠しながら、要素へのアクセスを統一されたインターフェースで提供するパターンです。 Java の java.util.Iterator を実装することで for-each ループが使えるようになります。

Iterator パターンとは

コレクション(リスト・木構造・グラフなど)の要素を順番に取り出す方法は、内部のデータ構造によって異なります。 Iterator パターンでは「次の要素があるか?」「次の要素を取り出す」という操作をhasNext()next() という統一されたインターフェースで提供します。 利用側はコレクションの内部構造を知らなくても同じ書き方で要素を走査できます。

Java での Iterator 実装

Java で自作コレクションを for-each ループに対応させるには、2つのインターフェースを実装します。

  • Iterable<T>: iterator() メソッドを実装する。for-each ループはこのメソッドを呼び出す
  • Iterator<T>: hasNext()next() を実装する。実際の走査ロジックをここに書く

Java 標準ライブラリの実例

  • java.util.Iterator: 全コレクション(ArrayList・HashSet・LinkedList など)が実装しているイテレータの基本インターフェース
  • java.util.Spliterator: Java 8 で追加された並列処理対応のイテレータ。Fork/Join フレームワークと組み合わせてデータを並列に走査できる
  • Stream API: Spliterator を内部で使用しており、filter()map() などの操作はイテレータパターンの考え方を応用している

for-each ループの仕組み

// for-each ループ
for (String item : store) {
    System.out.println(item);
}

// Java コンパイラが内部的に以下のコードに変換する
Iterator<String> it = store.iterator(); // Iterable の iterator() を呼び出す
while (it.hasNext()) {
    String item = it.next();
    System.out.println(item);
}

サンプルコード

IteratorPatternSample.java
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

public class IteratorPatternSample {

    // 汎用データストア: 内部のリスト構造を隠してイテレータを提供する
    static class DataStore<T> implements Iterable<T> {
        private final List<T> items = new ArrayList<T>();

        // 要素を追加する
        public void add(T item) {
            items.add(item);
        }

        // 要素数を返す
        public int size() {
            return items.size();
        }

        // Iterable を実装することで for-each ループが使えるようになる
        @Override
        public Iterator<T> iterator() {
            return new DataStoreIterator<T>(items);
        }
    }

    // DataStore 専用のイテレータ実装
    static class DataStoreIterator<T> implements Iterator<T> {
        private final List<T> items;
        private int currentIndex = 0; // 現在の走査位置

        public DataStoreIterator(List<T> items) {
            this.items = items;
        }

        // 次の要素があるかどうかを返す
        @Override
        public boolean hasNext() {
            return currentIndex < items.size();
        }

        // 次の要素を返し、カーソルを進める
        @Override
        public T next() {
            if (!hasNext()) {
                // hasNext() を確認せずに next() を呼ぶと NoSuchElementException が発生する
                throw new NoSuchElementException("これ以上要素がありません (index=" + currentIndex + ")");
            }
            return items.get(currentIndex++);
        }
    }

    // ページング機能付きデータストア: DataStore を継承して pageIterator を追加する
    static class PagedDataStore<T> extends DataStore<T> {
        private final int pageSize; // 1ページあたりの件数

        public PagedDataStore(int pageSize) {
            this.pageSize = pageSize;
        }

        // 指定ページ(0始まり)の要素だけをイテレートするイテレータを返す
        public Iterator<T> pageIterator(int page) {
            int start = page * pageSize;
            int end = Math.min(start + pageSize, size());
            List<T> pageItems = new ArrayList<T>();
            Iterator<T> all = iterator();
            int idx = 0;
            while (all.hasNext()) {
                T item = all.next();
                if (idx >= start && idx < end) {
                    pageItems.add(item);
                }
                idx++;
            }
            return new DataStoreIterator<T>(pageItems);
        }
    }

    public static void main(String[] args) {
        System.out.println("=== Iterator パターン: カスタムコレクションのイテレータ ===");

        // DataStore に要素を追加する
        DataStore<String> store = new DataStore<String>();
        store.add("Apple");
        store.add("Banana");
        store.add("Cherry");
        store.add("Date");
        store.add("Elderberry");

        System.out.println("--- for-each ループで全要素を走査 ---");
        // Iterable を実装しているので for-each ループが使える
        for (String item : store) {
            System.out.println("  " + item);
        }

        System.out.println("\n--- Iterator を直接使って走査 ---");
        Iterator<String> it = store.iterator();
        while (it.hasNext()) {
            // hasNext() で確認してから next() を呼ぶのが基本
            System.out.println("  " + it.next());
        }

        System.out.println("\n--- PagedDataStore: ページ単位で走査 ---");
        PagedDataStore<String> pagedStore = new PagedDataStore<String>(2);
        pagedStore.add("Page1-A");
        pagedStore.add("Page1-B");
        pagedStore.add("Page2-A");
        pagedStore.add("Page2-B");
        pagedStore.add("Page3-A");

        for (int page = 0; page * 2 < pagedStore.size(); page++) {
            System.out.println("ページ " + page + ":");
            Iterator<String> pageIt = pagedStore.pageIterator(page);
            while (pageIt.hasNext()) {
                System.out.println("  " + pageIt.next());
            }
        }

        System.out.println("\n--- 空のコレクションで hasNext() を確認 ---");
        DataStore<String> empty = new DataStore<String>();
        Iterator<String> emptyIt = empty.iterator();
        System.out.println("空コレクションの hasNext(): " + emptyIt.hasNext()); // false

        System.out.println("\n--- NoSuchElementException のデモ ---");
        DataStore<String> small = new DataStore<String>();
        small.add("only one");
        Iterator<String> smallIt = small.iterator();
        System.out.println("1件目: " + smallIt.next());
        try {
            // hasNext() を確認せずに next() を呼ぶと例外が発生する
            smallIt.next();
        } catch (NoSuchElementException e) {
            System.out.println("例外発生: " + e.getMessage());
        }
    }
}

Java 8 では Iterable<T> インターフェースを実装することで for-each ループが使えるようになります。Iterator<T> の実装では hasNext() で境界チェックを行い、next() で NoSuchElementException をスローする責務があります。

よくあるミス・注意点

⚠️ hasNext() を確認せずに next() を呼び出すと NoSuchElementException が発生する

next() は 次の要素がない状態で呼び出すと NoSuchElementException をスローします。 必ず hasNext() で確認してから next() を呼び出すようにしましょう。 for-each ループを使う場合はコンパイラが自動的に hasNext() を呼び出してくれるため、 この問題は発生しません。

⚠️ イテレーション中にコレクションを変更すると ConcurrentModificationException が発生する

ArrayList などの標準コレクションをイテレーション中に要素を追加・削除するとConcurrentModificationException が発生します。 要素を削除したい場合は Iteratorremove() メソッドを使うか、 Stream API の filter() で新しいリストを作成する方法を使いましょう。

⚠️ Iterator はステートフル(状態を持つ)なので使い回せない

一度終端まで走査した Iterator は再利用できません。 もう一度最初から走査したい場合は iterator() を呼び出して新しい Iterator を取得する必要があります。

テストする観点

  • 全要素を順番に取得できること(要素の順序が正しいこと)
  • hasNext() が正しく境界値を返すこと(最後の要素の前は true、最後の要素を取得した後は false)
  • 空のコレクションで hasNext() が false を返すこと(境界値: 要素数 0)
  • 要素が1件のコレクションで、next() を1回呼び出した後に hasNext() が false を返すこと(境界値: 要素数 1)
  • hasNext() を確認せずに next() を呼び出すと NoSuchElementException がスローされること
  • PagedDataStore でページ番号ごとに正しい要素が取得できること
  • for-each ループで全要素が走査できること(Iterable の実装が正しいこと)

GitHub でソースコードを見る →