Java | Java 標準ライブラリ:イテレータ

Java Java
スポンサーリンク

イテレータって何者かをざっくりイメージする

まず感覚からいきます。

イテレータ(Iterator)は、

「コレクションの中身を、順番に“たどる”ための共通ルール」

です。

ArrayList でも HashSet でも LinkedList でも、
「中の要素を順番に取り出したい」という場面は必ずありますよね。

でも、内部構造はクラスごとにバラバラです。

  • ArrayList…中身は配列
  • LinkedList…中身はリンク(前後のノード)
  • HashSet…中身はハッシュテーブル

それなのに、毎回構造に依存した書き方をしていたら大変です。

そこで登場するのが Iterator という「共通インターフェース」。

「次の要素があるか?」→ hasNext()
「次の要素を返して進む」→ next()
「今見ている要素を削除」→ remove()

という統一 API さえ守れば、
内部構造がどうであれ、同じ書き方で“なめて”いけるようにしましょう、という仕組みです。


Iterator の基本構造と典型的な使い方

Iterator の基本メソッド

java.util.Iterator<E> インターフェースは、主に 3 つのメソッドを持ちます。

  • boolean hasNext()
    まだ次の要素があるなら true、もう終わりなら false
  • E next()
    次の要素を返し、内部の位置を「一つ先」に進める
    もう要素がないのに呼ぶと NoSuchElementException
  • default 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 を理解してからで十分

タイトルとURLをコピーしました