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);
}サンプルコード
Java 8 では Iterable<T> インターフェースを実装することで for-each ループが使えるようになります。Iterator<T> の実装では hasNext() で境界チェックを行い、next() で NoSuchElementException をスローする責務があります。
よくあるミス・注意点
⚠️ hasNext() を確認せずに next() を呼び出すと NoSuchElementException が発生する
next() は 次の要素がない状態で呼び出すと NoSuchElementException をスローします。 必ず hasNext() で確認してから next() を呼び出すようにしましょう。 for-each ループを使う場合はコンパイラが自動的に hasNext() を呼び出してくれるため、 この問題は発生しません。
⚠️ イテレーション中にコレクションを変更すると ConcurrentModificationException が発生する
ArrayList などの標準コレクションをイテレーション中に要素を追加・削除するとConcurrentModificationException が発生します。 要素を削除したい場合は Iterator のremove() メソッドを使うか、 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 の実装が正しいこと)