概要

コレクションの内部構造を公開せずに、要素を1つずつ取り出せるようにする。Java の拡張 for 文(for-each)は Iterable インターフェースの iterator() メソッドを呼び出しており、Iterator パターンそのものです。標準の ArrayList や HashMap を使う限り意識する必要は薄いですが、独自のデータ構造を for-each で走査したい場合や、ページング付きのイテレーションが必要な場合には、Iterator を自作する場面が出てきます。この記事では、カスタムコレクションに Iterable を実装して for-each 対応にする方法と、record を使ったページ情報の表現、Java 21 の sealed interface で走査戦略を型安全に定義する方法を整理します。

使いどころ

自作のツリー構造やグラフ構造を for-each ループで走査できるよう、Iterable インターフェースを実装する

検索結果を1ページずつ取得するページングイテレータを実装し、大量データの一括取得を避ける

データベースの ResultSet をラップし、ビジネスオブジェクトの Iterator として返す共通ユーティリティを作る

コード例

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

public class IteratorPatternDemo {

    static class DataStore<T> implements Iterable<T> {
        private final List<T> items = new ArrayList<>();
        public void add(T item) { items.add(item); }
        public int size() { return items.size(); }

        @Override
        public Iterator<T> iterator() {
            return new DataStoreIterator<>(items);
        }
    }

    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()) {
                throw new NoSuchElementException(
                    "index=" + currentIndex);
            }
            return items.get(currentIndex++);
        }
    }

    record Page<T>(int pageNumber, List<T> items,
                   boolean hasNext) {}

    static class PagedDataStore<T> extends DataStore<T> {
        private final int pageSize;
        public PagedDataStore(int pageSize) {
            this.pageSize = pageSize;
        }
        public Page<T> getPage(int pageNumber) {
            int start = pageNumber * pageSize;
            int end = Math.min(start + pageSize, size());
            var pageItems = new ArrayList<T>();
            var all = iterator();
            int idx = 0;
            while (all.hasNext()) {
                T item = all.next();
                if (idx >= start && idx < end) {
                    pageItems.add(item);
                }
                idx++;
            }
            return new Page<>(pageNumber, pageItems,
                              end < size());
        }
    }

    public static void main(String[] args) {
        var store = new DataStore<String>();
        store.add("Apple");
        store.add("Banana");
        store.add("Cherry");

        for (var item : store) {
            System.out.println(item);
        }

        var paged = new PagedDataStore<String>(2);
        paged.add("A"); paged.add("B"); paged.add("C");
        paged.add("D"); paged.add("E");

        int pageNum = 0;
        Page<String> page;
        do {
            page = paged.getPage(pageNum);
            System.out.println("ページ" + page.pageNumber()
                + ": " + page.items());
            pageNum++;
        } while (page.hasNext());
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record で Page<T> を定義でき、ページ番号・要素リスト・次ページ有無を簡潔に表現できる。var で型推論も可。

Java 17
// Java 17: record でページ情報をまとめて返す
record Page<T>(int pageNumber, List<T> items,
               boolean hasNext) {}

public Page<T> getPage(int pageNumber) {
    int start = pageNumber * pageSize;
    int end = Math.min(start + pageSize, size());
    // ... 要素を収集
    return new Page<>(pageNumber, pageItems,
                      end < size());
}

Library Comparison

標準 API(Iterable / Iterator)for-each 対応とカスタム走査の実装。外部依存なしで完結する。フィルタリングや変換を組み合わせると Stream API の方が宣言的に書ける。
Stream APIfilter / map / collect の組み合わせで宣言的にデータ処理したい場合。一度しか消費できない点は Iterator と同じ。再利用が必要なら Supplier<Stream> でラップする。
Guava(AbstractIterator)computeNext() を実装するだけでカスタムイテレータを簡潔に作りたい場合。Guava への依存が増える。Java 標準だけで十分な場面が多い。

注意点

hasNext() を呼ばずに next() を呼ぶと NoSuchElementException が発生する。ドキュメントやコードレビューで必ず hasNext() チェックを徹底する

Iterator は1回限りの走査が基本。複数回走査したい場合は Iterable.iterator() で新しいインスタンスを生成する設計にすること

走査中にコレクションを変更すると ConcurrentModificationException が起きる。削除が必要な場合は Iterator.remove() を使う

PagedDataStore のように走査単位を変えたイテレータを提供する場合、元データが変更されたときの一貫性をどう保証するか設計で決めておく

FAQ

for-each ループと Iterator の直接操作はどう使い分けますか。

通常は for-each で十分です。走査中に要素を削除したい場合や、hasNext() の結果で処理を分岐したい場合に限り Iterator を直接使います。

Spliterator とは何ですか。

Java 8 で追加された並列走査用のインターフェースです。Stream の内部で使われており、通常の業務コードで直接実装する機会は少ないです。

remove() を実装しない場合はどうすればよいですか。

Java 8 以降では Iterator.remove() にデフォルト実装(UnsupportedOperationException をスロー)があるため、実装しなくてもコンパイルは通ります。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます