概要
コレクションの内部構造を公開せずに、要素を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 として返す共通ユーティリティを作る
コード例
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());
}
}Version Coverage
record で Page<T> を定義でき、ページ番号・要素リスト・次ページ有無を簡潔に表現できる。var で型推論も可。
// 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
注意点
hasNext() を呼ばずに next() を呼ぶと NoSuchElementException が発生する。ドキュメントやコードレビューで必ず hasNext() チェックを徹底する
Iterator は1回限りの走査が基本。複数回走査したい場合は Iterable.iterator() で新しいインスタンスを生成する設計にすること
走査中にコレクションを変更すると ConcurrentModificationException が起きる。削除が必要な場合は Iterator.remove() を使う
PagedDataStore のように走査単位を変えたイテレータを提供する場合、元データが変更されたときの一貫性をどう保証するか設計で決めておく
FAQ
通常は for-each で十分です。走査中に要素を削除したい場合や、hasNext() の結果で処理を分岐したい場合に限り Iterator を直接使います。
Java 8 で追加された並列走査用のインターフェースです。Stream の内部で使われており、通常の業務コードで直接実装する機会は少ないです。
Java 8 以降では Iterator.remove() にデフォルト実装(UnsupportedOperationException をスロー)があるため、実装しなくてもコンパイルは通ります。