イテレータって何者かをざっくりイメージする
まず感覚からいきます。
イテレータ(Iterator)は、
「コレクションの中身を、順番に“たどる”ための共通ルール」
です。
ArrayList でも HashSet でも LinkedList でも、
「中の要素を順番に取り出したい」という場面は必ずありますよね。
でも、内部構造はクラスごとにバラバラです。
- ArrayList…中身は配列
- LinkedList…中身はリンク(前後のノード)
- HashSet…中身はハッシュテーブル
それなのに、毎回構造に依存した書き方をしていたら大変です。
そこで登場するのが Iterator という「共通インターフェース」。
「次の要素があるか?」→ hasNext()
「次の要素を返して進む」→ next()
「今見ている要素を削除」→ remove()
という統一 API さえ守れば、
内部構造がどうであれ、同じ書き方で“なめて”いけるようにしましょう、という仕組みです。
Iterator の基本構造と典型的な使い方
Iterator の基本メソッド
java.util.Iterator<E> インターフェースは、主に 3 つのメソッドを持ちます。
boolean hasNext()
まだ次の要素があるならtrue、もう終わりならfalseE next()
次の要素を返し、内部の位置を「一つ先」に進める
もう要素がないのに呼ぶとNoSuchElementExceptiondefault void remove()
「直前に返した要素」をコレクションから削除(対応していない実装もある)
概要はこれだけです。
ArrayList で Iterator を使う例
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class IteratorBasic {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Alice");
list.add("Bob");
list.add("Carol");
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String name = it.next();
System.out.println(name);
}
}
}
Javaポイントは、
list.iterator()で「イテレータを取得」while (it.hasNext())で「まだ要素がある間」繰り返しit.next()で一つずつ要素を取り出す
というパターンです。
List だろうが Set だろうが、iterator() が返すものに対して
この 3 ステップで同じように書けます。
拡張 for 文(for-each)との関係
実は for-each の裏で Iterator が動いている
よく書くこの形:
for (String name : list) {
System.out.println(name);
}
Javaこれは、コンパイラが「裏で Iterator を使うコード」に変換しています。
イメージとしては、コンパイラが勝手にこう書き換えていると思っていいです。
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
String name = it.next();
System.out.println(name);
}
Javaつまり、
for (型 変数 : コレクション)と書けるコレクションは
「Iterator を提供している(=Iterableを実装している)」ということ- Iterator を知ると、「for-each の裏側」が見える
という関係になっています。
なので、「普段は for-each で書いておいて、Iterator が必要な場面だけ素の Iterator を使う」
というスタンスで大丈夫です。
Iterator が「真価を発揮する」場面:ループ中の削除
ここは重要なので、少し丁寧に説明します。
普通の for-each で remove するとほぼ確実に事故る
例えば、「リストの中から、特定の条件を満たす要素を削除したい」
という場面を考えます。
初心者がやりがちなのはこれです。
for (String s : list) {
if (s.startsWith("A")) {
list.remove(s); // 危険!
}
}
Javaこれを実行すると、多くの場合ConcurrentModificationException が飛びます。
理由は、
- for-each(Iterator)は「内部状態」を持ちながらループしている
- その最中に、別ルート(ここでは
list.remove)からコレクションを変更すると、「状態がズレる」
という「不整合」が起きるからです。
Java の Iterator はこの不整合を見つけると、fail-fast といって、
すぐに例外を投げて落ちるようになっています。
正しい書き方:Iterator の remove を使う
ループ中に安全に削除したいときは、「Iterator 自身の remove()」を使います。
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.startsWith("A")) {
it.remove(); // 今返した要素を削除(安全)
}
}
Javaここでのポイントは、
next()で「今の要素」を取り出す- 条件に合致したら
it.remove()(Iterator に削除を任せる) list.remove(...)のように、外側のコレクションに直接触らない
というところです。
Iterator 自身が内部の状態と整合性を取りながら削除してくれるので、ConcurrentModificationException を避けることができます。
これは Iterator を学ぶ最大の理由の一つです。
Iterator の「fail-fast」な性質をもう少しだけ深掘り
なぜ「途中でコレクションをいじると怒る」のか
多くのコレクション(特に ArrayList, HashSet, HashMap など)の Iterator は
fail-fast という性質を持っています。
イメージとしては、
- Iterator が作られた時点で「変更回数のカウンタ」を覚える
- ループ中にコレクションが別ルートで変更されると、カウンタが変わる
next()やhasNext()を呼んだときに「カウンタずれてる!」と気づく- その瞬間
ConcurrentModificationExceptionを投げる
という動きをしています。
これは、「おかしな状態で処理を続けて、気づきにくいバグを生むくらいなら、
早めに落ちてくれたほうがマシ」という思想です。
どうすれば避けられるか
避け方はシンプルで、
- ループ中にコレクションを直接操作しない
- 削除したいなら Iterator の
remove()を使う - どうしても別ルートで変更するなら、そもそもの設計を見直す
ということです。
初心者のうちは、
「for-each ループ中に List / Set / Map を直接いじらない」
「削除は Iterator の remove でやる」
というルールを自分に課しておくと、かなりバグを避けられます。
ListIterator との違い(「前後に動きたい」場合に使う)
ListIterator は「List 専用の高機能イテレータ」
Iterator は「前に進むだけ」のシンプルな仕組みです。
List 専用には、もう一つ ListIterator というものがあります。
ListIterator は、
- 前方向にも後ろ方向にも進める(
hasPrevious,previous) - インデックス情報を持っている(
nextIndex,previousIndex) - イテレータの位置に対して
add,setで挿入・更新できる
といった、よりリッチな API を持ちます。
例として、List を前から・後ろから両方向に走査することができます。
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
ListIterator<String> it = list.listIterator(list.size());
while (it.hasPrevious()) {
System.out.println(it.previous()); // C, B, A と逆順に出る
}
Javaただし、初心者の段階では、
- 「まず Iterator を理解」
- 「必要になったら ListIterator も見る」
くらいで十分です。
普段は拡張 for 文(for-each)で事足ります。
まとめ:Iterator を頭の中でどう位置づけるか
イテレータを初心者向けに一言でまとめると、
「コレクションの中身を、内部構造に依存せず“順番にたどる”ための共通インターフェース」
です。
特に意識しておきたいポイントは次のようなところです。
hasNext()とnext()の 2 ステップで、どんなコレクションも同じように走査できる- 拡張 for 文(for-each)は、コンパイラが裏で Iterator に展開している
- ループ中に安全に削除したいなら、Iterator の
remove()を使う(ここが実務で超重要) - Iterator は fail-fast なので、ループ中にコレクションを直接いじると
ConcurrentModificationExceptionが出る - List 専用の高機能版として
ListIteratorもいるが、まずは Iterator を理解してからで十分
