Iterator(イテレータ)とは、コレクションの要素を先頭から1つずつ順番に取り出すための仕組みで、hasNext() で「次があるか」を確かめ、next() で要素を受け取ります。 拡張 for 文(for (String s : list))も内部ではこの Iterator を使っており、ふだんは意識せずに済みます。Iterator を直接書く場面は主に2つで、ループしながら要素を削除したいときと、自作クラスを拡張 for 文で回せるようにしたいときです。

この記事では、拡張 for 文の中で list.remove() を呼んで ConcurrentModificationException が出る様子をスタックトレースごと再現し、iterator.remove()・removeIf・逆順 for・Stream の filter という4つの直し方を実行結果つきで比較します。さらに、Iterable を自作して拡張 for 文で回す最小コード、ListIterator の双方向走査と set / add、Map の entrySet 走査、next() の呼び忘れなどの典型的なミスまでを1ページにまとめました。コードはすべて OpenJDK 21.0.10 で java ファイル名.java として実行した結果です。

Iteratorの早見表

やりたいこと 書き方 補足
イテレータを取り出す Iterator<String> it = list.iterator(); List・Set・Queue など Collection 全般で使える
次の要素があるか調べる it.hasNext() true / false を返す。位置は進まない
次の要素を取り出す String s = it.next(); 位置が1つ進む。要素がないと NoSuchElementException
ループ中に削除する it.remove(); 直前の next() で受け取った要素を削除
条件に合う要素をまとめて削除 list.removeIf(s -> 条件) Java 8 以降。もっとも短く書ける
残りをまとめて処理 it.forEachRemaining(s -> ...) Java 8 以降
前後両方向に走査 ListIterator<String> it = list.listIterator(); List 専用。hasPrevious() / previous()
ループ中に置き換え・挿入 listIt.set(x) / listIt.add(x) ListIterator のみ
Map を走査 map.entrySet().iterator() Map.Entry の getKey() / getValue()
自作クラスを拡張 for 文で回す implements Iterable<T> して iterator() を実装 forEach も使えるようになる

ループ中の削除で迷ったら removeIf を選ぶ、削除と同時に別の処理もしたいなら iterator.remove()、というのが基本の目安です。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java Iteratorの使い方|ループ中の削除とIterable実装」

Iteratorの基本:hasNext・next・remove

Iterator は java.util.Iterator<E> というインターフェースです。Java SE 21 の API ドキュメントで確認すると、メソッドは次の4つです。

メソッド 役割 例外
boolean hasNext() 次の要素があれば true なし
E next() 次の要素を返し、位置を1つ進める 要素がないと NoSuchElementException
default void remove() 直前の next() で返した要素を元のコレクションから削除(任意の操作) 未対応なら UnsupportedOperationException、next() の前や2回続けて呼ぶと IllegalStateException
default void forEachRemaining(Consumer) 残りの全要素に処理を適用 引数が null なら NullPointerException

実装が必須なのは hasNext() と next() の2つだけで、remove() の既定の実装は UnsupportedOperationException を投げるようになっています。

whileループで回す基本形

次のコードは Basic.java として保存し、java Basic.java でそのまま実行できます。

import java.util.Iterator;
import java.util.List;

public class Basic {
    public static void main(String[] args) {
        List<String> names = List.of("pen", "note", "clip");

        Iterator<String> it = names.iterator();   // 1. イテレータを取り出す
        while (it.hasNext()) {                    // 2. 次の要素があるか確認
            String name = it.next();              // 3. 次の要素を受け取り、1つ進む
            System.out.println(name);
        }

        // 同じ処理を拡張 for 文で書いた場合(内部では上と同じことをしている)
        for (String name : names) {
            System.out.println(name);
        }

        // 走査を終えたイテレータは使い回せない
        System.out.println(it.hasNext());
        try {
            it.next();
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}

実行結果(OpenJDK 21.0.10):

pen
note
clip
pen
note
clip
false
java.util.NoSuchElementException

Iterator は「いまどこまで読んだか」という位置を持つ、使い切りのオブジェクトです。 最後まで進んだイテレータは hasNext() が false のままで、巻き戻す方法はありません。もう一度先頭から回したいときは、list.iterator() を呼び直して新しいイテレータを受け取ります。

位置の動きを表にすると次のようになります。▲ がイテレータの現在位置で、next() は「位置の右隣の要素を返して、位置を右へ1つ動かす」操作です。

操作 返る値 操作後の位置
list.iterator() (イテレータ) ▲ pen note clip
next() pen pen ▲ note clip
next() note pen note ▲ clip
next() clip pen note clip ▲
hasNext() false pen note clip ▲(動かない)

拡張for文・forEach・Iteratorの使い分け

書き方 向いている場面 ループ中の削除
拡張 for 文 for (T x : list) 全要素を読むだけ。もっとも読みやすい できない(例外になる)
list.forEach(x -> ...) 1要素につき1つの短い処理 できない(例外になる)
Iterator + while 走査しながら削除したい it.remove() でできる
ListIterator 逆方向の走査、置き換え、挿入をしたい remove() / set() / add() ができる
インデックスの for 文 位置(何番目か)が必要 後ろから回せばできる

拡張for文の中でremoveするとConcurrentModificationExceptionになる

「完了済みのタスクをリストから取り除く」という処理を、拡張 for 文で素直に書いてみます。

import java.util.ArrayList;
import java.util.List;

public class Cme {
    public static void main(String[] args) {
        List<String> tasks = new ArrayList<>(
                List.of("write", "done:review", "test", "done:deploy", "plan"));

        for (String t : tasks) {
            if (t.startsWith("done:")) {
                tasks.remove(t);       // ループ中に List 本体から削除
            }
        }
        System.out.println(tasks);
    }
}

実行結果:

Exception in thread "main" java.util.ConcurrentModificationException
    at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1095)
    at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1049)
    at Cme.main(Cme.java:9)

コンパイルは通りますが、実行すると ConcurrentModificationException で止まります。 スタックトレースを下から読むと、仕組みが見えてきます。

行 読み取れること
at Cme.main(Cme.java:9) 例外が出たのは9行目の for (String t : tasks)。tasks.remove(t) の行ではない
at ...ArrayList$Itr.next 拡張 for 文が内部で呼んでいる Iterator.next() の中で発生
at ...ArrayList$Itr.checkForComodification 「走査中に本体が変更されていないか」の確認で検出された

なぜ例外になるのか

ArrayList は、要素の追加・削除といった構造の変更があるたびに、内部の変更回数カウンタを1つ増やします。イテレータは作られた時点のカウンタ値を覚えておき、next() のたびに現在の値と比べます。イテレータを通さずに tasks.remove(t) で本体を変更すると値がずれ、次の next() で検出される、という流れです。削除の行ではなく、次の周回の for の行で例外が出るのはこのためです。

Java SE 21 の API ドキュメント(ConcurrentModificationException)にも、この例外は別スレッドからの変更だけを意味するのではなく、1つのスレッドがフェイルファスト(問題を早めに知らせる方式)のイテレータで走査している最中にコレクションを直接変更した場合にも投げられる、と書かれています。名前に Concurrent とありますが、スレッドを1つしか使っていなくても起きる点がポイントです。

例外が出ないのに結果がずれるケース

同じ「ループ中の削除」でも、要素の位置によっては例外が出ないことがあります。気づきにくいので、あわせて再現しておきます。

import java.util.ArrayList;
import java.util.List;

public class Quirk {
    public static void main(String[] args) {
        // A. 後ろから2番目の要素を削除 → 例外は出ないが、最後の "d" は走査されない
        List<String> a = new ArrayList<>(List.of("a", "b", "done:c", "d"));
        for (String t : a) {
            System.out.println("A visit " + t);
            if (t.startsWith("done:")) a.remove(t);
        }
        System.out.println("A " + a);

        // B. 最後の要素を削除 → 削除は済んでいるが、その後で例外
        List<String> b = new ArrayList<>(List.of("a", "b", "done:c"));
        try {
            for (String t : b) {
                if (t.startsWith("done:")) b.remove(t);
            }
        } catch (Exception e) {
            System.out.println("B " + e + " / " + b);
        }

        // C. 前からのインデックス for → 例外は出ないが、連続した対象を1つ飛ばす
        List<String> c = new ArrayList<>(List.of("a", "done:1", "done:2", "b"));
        for (int i = 0; i < c.size(); i++) {
            if (c.get(i).startsWith("done:")) c.remove(i);
        }
        System.out.println("C " + c);

        // D. ループ中の add、forEach 中の remove も同じ例外
        List<String> d = new ArrayList<>(List.of("a", "b"));
        try {
            for (String t : d) d.add("x");
        } catch (Exception e) {
            System.out.println("D " + e);
        }
        List<String> f = new ArrayList<>(List.of("a", "b", "c"));
        try {
            f.forEach(s -> { if (s.equals("b")) f.remove(s); });
        } catch (Exception e) {
            System.out.println("E " + e);
        }
    }
}

実行結果:

A visit a
A visit b
A visit done:c
A [a, b, d]
B java.util.ConcurrentModificationException / [a, b]
C [a, done:2, b]
D java.util.ConcurrentModificationException
E java.util.ConcurrentModificationException
ケース 結果 理由
A. 後ろから2番目を削除 例外なし。ただし最後の d は一度も処理されない 削除でサイズが1つ減り、hasNext() が「もう終わり」と判定してループを抜けるため、確認が行われる next() が呼ばれない
B. 最後の要素を削除 削除後に例外 サイズと位置がずれて hasNext() が true になり、次の next() で検出される
C. 前からインデックスで削除 例外なし。done:2 が残る 削除で後ろの要素が1つ前に詰まり、i++ でその要素を飛ばす
D. ループ中に add 例外 追加も構造の変更に当たる
E. forEach の中で remove 例外 forEach も走査中の変更を確認している

A と C は例外が出ないまま結果だけがずれるため、テストデータによっては気づけません。API ドキュメントにも、フェイルファストの動作は保証されたものではなく、この例外はバグの検出にだけ使うべき、という趣旨の説明があります。「例外が出なかったから問題ない」とは考えず、次の節の書き方に置き換えるのが確実です。

ループ中に要素を削除する4つの方法

同じデータに対して、4つの方法で「done: で始まる要素を取り除く」処理を書きます。

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

public class FourWays {
    static List<String> init() {
        return new ArrayList<>(List.of("write", "done:review", "test", "done:deploy", "plan"));
    }

    public static void main(String[] args) {
        // 1. iterator.remove()
        List<String> t1 = init();
        Iterator<String> it = t1.iterator();
        while (it.hasNext()) {
            String t = it.next();
            if (t.startsWith("done:")) {
                it.remove();               // 直前に next() で受け取った要素を削除
            }
        }
        System.out.println("1 iterator.remove : " + t1);

        // 2. removeIf(Java 8 以降)
        List<String> t2 = init();
        boolean changed = t2.removeIf(t -> t.startsWith("done:"));
        System.out.println("2 removeIf        : " + t2 + " 戻り値=" + changed);

        // 3. 逆順の for 文(インデックスで後ろから)
        List<String> t3 = init();
        for (int i = t3.size() - 1; i >= 0; i--) {
            if (t3.get(i).startsWith("done:")) {
                t3.remove(i);
            }
        }
        System.out.println("3 逆順for          : " + t3);

        // 4. Stream の filter(残したい条件を書き、新しい List を作る)
        List<String> t4 = init();
        List<String> kept = t4.stream()
                .filter(t -> !t.startsWith("done:"))
                .toList();
        System.out.println("4 Stream filter   : " + kept);
        System.out.println("  元のList         : " + t4);
        try {
            kept.add("x");
        } catch (Exception e) {
            System.out.println("  toList()の結果に add : " + e);
        }
    }
}

実行結果:

1 iterator.remove : [write, test, plan]
2 removeIf        : [write, test, plan] 戻り値=true
3 逆順for          : [write, test, plan]
4 Stream filter   : [write, test, plan]
  元のList         : [write, done:review, test, done:deploy, plan]
  toList()の結果に add : java.lang.UnsupportedOperationException

4つとも [write, test, plan] になりました。違いは次の表のとおりです。

4つの方法の比較表

方法 元の List 条件の書き方 使える対象 向いている場面
1. iterator.remove() 変更される 削除する条件 すべての変更可能な Collection 削除と同時に、ログ出力や集計など別の処理もしたい
2. removeIf 変更される 削除する条件 すべての変更可能な Collection 条件で削除するだけ。まずこれを検討
3. 逆順 for 変更される 削除する条件 List(インデックスで操作できるもの) 何番目かの情報も使いたい
4. Stream filter 変更されない(新しい List ができる) 残す条件 Stream にできるものすべて 元のデータを保ったまま、絞り込んだ結果が欲しい

それぞれの注意点も整理します。

方法 気をつける点
iterator.remove() next() を呼んだ後に1回だけ呼べる。拡張 for 文ではなく while で書く
removeIf 戻り値は「1件でも削除したか」の boolean。List.of(...) などの変更できない List では UnsupportedOperationException
逆順 for i-- と i >= 0 を間違えやすい。List<Integer> では remove(int)(位置)と remove(Object)(値)の取り違えに注意
Stream filter 条件が「残す側」なので ! を付け忘れない。toList()(Java 16 以降)の結果は変更できない。変更したい場合は collect(Collectors.toCollection(ArrayList::new))

後ろから回す逆順 for が正しく動くのは、削除しても「これから見る前側の要素」の位置がずれないからです。前から回すと、前節のケース C のように削除直後の要素を飛ばしてしまいます。

Iterableを自作して拡張for文で回す

拡張 for 文の : の右側に置けるのは、配列か、Iterable インターフェースを実装したオブジェクトです。Iterable<T> で実装が必要なメソッドは iterator() の1つだけなので、自作クラスも数行で拡張 for 文に対応できます。

インターフェース 役割 実装するメソッド
Iterable<T> 「順番に取り出せるもの」を表す。拡張 for 文の対象になれる Iterator<T> iterator()
Iterator<T> 「いまどこまで読んだか」を持ち、実際に1つずつ取り出す hasNext()、next()

名前は似ていますが、Iterable は「回せる側」、Iterator は「回す道具」です。List や Set は Iterable を継承しているので、拡張 for 文で回せます。

最小コード:整数の範囲を返すRange

import java.util.Iterator;
import java.util.NoSuchElementException;

public class RangeDemo {
    public static void main(String[] args) {
        // 拡張 for 文で回せる
        for (int n : new Range(1, 4)) {
            System.out.println(n);
        }

        // Iterable の default メソッド forEach も使える
        new Range(10, 12).forEach(n -> System.out.println("forEach: " + n));

        // 何度でも最初から回せる(iterator() が毎回新しいイテレータを返すため)
        Range r = new Range(1, 3);
        int sum = 0;
        for (int n : r) sum += n;
        for (int n : r) sum += n;
        System.out.println("sum=" + sum);

        // 要素が尽きたあとの next() と、未対応の remove()
        Iterator<Integer> it = new Range(1, 2).iterator();
        it.next();
        try { it.next(); } catch (Exception e) { System.out.println(e); }
        try { it.remove(); } catch (Exception e) { System.out.println(e); }
    }
}

/** start 以上 end 未満の整数を順に返す */
class Range implements Iterable<Integer> {
    private final int start;
    private final int end;

    Range(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    public Iterator<Integer> iterator() {
        return new Iterator<>() {
            private int current = start;          // 走査の位置はイテレータ側に持つ

            @Override
            public boolean hasNext() {
                return current < end;
            }

            @Override
            public Integer next() {
                if (!hasNext()) {
                    throw new NoSuchElementException();
                }
                return current++;
            }
        };
    }
}

実行結果:

1
2
3
forEach: 10
forEach: 11
sum=6
java.util.NoSuchElementException
java.lang.UnsupportedOperationException: remove

Iterableを自作するときのチェックリスト

確認すること 理由
走査位置(current)は Iterator 側のフィールドにする Iterable 側に持つと、2回目のループや入れ子のループで位置が共有されてしまう
iterator() は呼ばれるたびに新しいイテレータを返す 上の例で sum=6(3+3)になるのは、2回とも先頭から回せているため
next() は要素がないとき NoSuchElementException を投げる Iterator の取り決め。hasNext() を確認せずに呼ぶ利用者にも結果を伝えられる
hasNext() は何回呼ばれても結果が変わらないようにする 位置を進めるのは next() だけにする
remove() は必要なときだけ実装する 実装しなければ、既定で UnsupportedOperationException になる(上の実行結果の最終行)

内部のListに任せる書き方とラムダ式

自作クラスが内部に List を持っているなら、その List のイテレータを返すだけで済みます。

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.stream.StreamSupport;

public class ShelfDemo {
    public static void main(String[] args) {
        Shelf shelf = new Shelf();
        shelf.add("Java");
        shelf.add("SQL");

        for (String book : shelf) {
            System.out.println(book);
        }

        // Iterable から Stream を作る
        List<String> upper = StreamSupport.stream(shelf.spliterator(), false)
                .map(String::toUpperCase)
                .toList();
        System.out.println(upper);

        // Iterable はメソッドが1つなので、ラムダ式でも書ける
        Iterable<String> letters = () -> List.of("x", "y").iterator();
        for (String s : letters) {
            System.out.println(s);
        }
    }
}

class Shelf implements Iterable<String> {
    private final List<String> books = new ArrayList<>();

    void add(String book) {
        books.add(book);
    }

    @Override
    public Iterator<String> iterator() {
        return books.iterator();      // 内部の List のイテレータをそのまま返す
    }
}

実行結果:

Java
SQL
[JAVA, SQL]
x
y

内部の List を外へ直接渡さずに、走査だけを公開できるのが Iterable を実装する利点です。なお、books.iterator() をそのまま返すと、利用者が remove() で内部の List から要素を削除できます。削除を受け付けたくない場合は Collections.unmodifiableList(books).iterator() を返す方法があります(この1行は未実行で、一般に知られた書き方です)。

ListIteratorで双方向に走査し、set・addで書き換える

ListIterator は List 専用のイテレータで、Iterator の機能に加えて逆方向への移動、要素の置き換え、挿入、インデックスの取得ができます。

メソッド 役割
hasNext() / next() 前から後ろへ進む
hasPrevious() / previous() 後ろから前へ戻る
nextIndex() / previousIndex() 次に返す要素・1つ前の要素のインデックス
set(e) 直前に next() / previous() で受け取った要素を置き換える
add(e) 現在の位置に要素を挿入する
remove() 直前に受け取った要素を削除する
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

public class ListIteratorDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(List.of("a", "b", "c"));

        // 前から後ろへ(インデックスも取れる)
        ListIterator<String> it = list.listIterator();
        while (it.hasNext()) {
            int index = it.nextIndex();
            System.out.println("→ " + index + ":" + it.next());
        }

        // そのまま後ろから前へ
        while (it.hasPrevious()) {
            int index = it.previousIndex();
            System.out.println("← " + index + ":" + it.previous());
        }

        // 走査しながら置き換え(set)と挿入(add)
        ListIterator<String> edit = list.listIterator();
        while (edit.hasNext()) {
            String s = edit.next();
            if (s.equals("b")) {
                edit.set("B");        // 直前に受け取った要素を置き換える
                edit.add("b2");       // 現在位置(B の直後)に挿入する
            }
        }
        System.out.println(list);

        // 末尾から逆順に走査(開始位置を指定)
        ListIterator<String> back = list.listIterator(list.size());
        StringBuilder sb = new StringBuilder();
        while (back.hasPrevious()) {
            sb.append(back.previous()).append(' ');
        }
        System.out.println(sb.toString().trim());

        // add の直後に set を呼ぶと例外
        ListIterator<String> bad = list.listIterator();
        bad.next();
        bad.add("z");
        try {
            bad.set("q");
        } catch (Exception e) {
            System.out.println(e);
        }
        System.out.println(list);
    }
}

実行結果:

→ 0:a
→ 1:b
→ 2:c
← 2:c
← 1:b
← 0:a
[a, B, b2, c]
c b2 B a
java.lang.IllegalStateException
[a, z, B, b2, c]

拡張 for 文の中で list.set() や list.add() を使う代わりに、ListIterator の set() / add() を使えば、走査を続けたまま安全に書き換えられます。 add() で挿入した要素(上の b2)は、その後の next() では返されないので、挿入した要素をもう一度処理してしまうこともありません。

set() と remove() は「直前に受け取った要素」が対象なので、add() や remove() を呼んだ直後は対象がなくなり、IllegalStateException になります(実行結果の9行目)。順番は next() → set() → add() と覚えておくとよいです。

MapをentrySetとIteratorで走査・削除する

Map 自体は Iterable ではないため、entrySet()(キーと値の組)、keySet()(キー)、values()(値)のいずれかを取り出して走査します。Map のループ方法そのものは別の記事で詳しく扱っているので、ここでは Iterator が関わる削除と書き換えに絞ります。

import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

public class MapIter {
    static Map<String, Integer> init() {
        Map<String, Integer> stock = new LinkedHashMap<>();
        stock.put("pen", 0);
        stock.put("note", 12);
        stock.put("clip", 0);
        stock.put("tape", 3);
        return stock;
    }

    public static void main(String[] args) {
        // 1. entrySet を拡張 for 文で走査
        for (Map.Entry<String, Integer> e : init().entrySet()) {
            System.out.println(e.getKey() + "=" + e.getValue());
        }

        // 2. 走査中に Map 本体から remove すると例外
        Map<String, Integer> m2 = init();
        try {
            for (String key : m2.keySet()) {
                if (m2.get(key) == 0) m2.remove(key);
            }
        } catch (Exception e) {
            System.out.println("2: " + e);
        }

        // 3. entrySet のイテレータで削除し、残す要素は値を書き換える
        Map<String, Integer> m3 = init();
        Iterator<Map.Entry<String, Integer>> it = m3.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, Integer> e = it.next();
            if (e.getValue() == 0) {
                it.remove();
            } else {
                e.setValue(e.getValue() + 100);
            }
        }
        System.out.println("3: " + m3);

        // 4. removeIf で1行に
        Map<String, Integer> m4 = init();
        m4.entrySet().removeIf(e -> e.getValue() == 0);
        System.out.println("4: " + m4);

        Map<String, Integer> m5 = init();
        m5.values().removeIf(v -> v == 0);
        System.out.println("5: " + m5);

        Map<String, Integer> m6 = init();
        m6.keySet().removeIf(k -> k.startsWith("c"));
        System.out.println("6: " + m6);
    }
}

実行結果:

pen=0
note=12
clip=0
tape=3
2: java.util.ConcurrentModificationException
3: {note=112, tape=103}
4: {note=12, tape=3}
5: {note=12, tape=3}
6: {pen=0, note=12, tape=3}
やりたいこと 書き方
キーと値を読む for (Map.Entry<K, V> e : map.entrySet())
走査しながら削除 map.entrySet().iterator() の it.remove()
走査しながら値を書き換え entry.setValue(新しい値)
値の条件でまとめて削除 map.values().removeIf(v -> 条件)
キーの条件でまとめて削除 map.keySet().removeIf(k -> 条件)
キーと値の両方の条件で削除 map.entrySet().removeIf(e -> 条件)

entrySet()・keySet()・values() は Map の「ビュー」で、ここから削除すると Map 本体からも消えます。List と同じく、走査中に map.remove(key) を直接呼ぶと ConcurrentModificationException になるので、イテレータか removeIf を使います。

Iteratorでよくあるミスと直し方

典型的なつまずきを1つのプログラムで再現しました。

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class Mistakes {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(List.of("a", "b", "c"));

        // 1. 1回のループで next() を2回呼ぶ
        Iterator<String> i1 = list.iterator();
        try {
            while (i1.hasNext()) {
                System.out.println("1: " + i1.next() + "-" + i1.next());
            }
        } catch (Exception e) {
            System.out.println("1: " + e);
        }

        // 2. next() の前に remove()
        Iterator<String> i2 = list.iterator();
        try {
            i2.remove();
        } catch (Exception e) {
            System.out.println("2: " + e);
        }

        // 3. remove() を続けて2回
        List<String> copy = new ArrayList<>(list);
        Iterator<String> i3 = copy.iterator();
        i3.next();
        i3.remove();
        try {
            i3.remove();
        } catch (Exception e) {
            System.out.println("3: " + e);
        }

        // 4. 変更できない List で remove / removeIf
        List<String> fixed = List.of("a", "b");
        Iterator<String> i4 = fixed.iterator();
        i4.next();
        try {
            i4.remove();
        } catch (Exception e) {
            System.out.println("4: " + e);
        }
        try {
            Arrays.asList("a", "b").removeIf(s -> s.equals("a"));
        } catch (Exception e) {
            System.out.println("5: " + e);
        }

        // 6. forEachRemaining で残りをまとめて処理
        Iterator<String> i6 = list.iterator();
        i6.next();
        i6.forEachRemaining(s -> System.out.println("6: " + s));

        // 7. CopyOnWriteArrayList は走査中に本体を変更しても例外にならない
        List<String> cow = new CopyOnWriteArrayList<>(List.of("a", "done:b", "c"));
        for (String s : cow) {
            if (s.startsWith("done:")) cow.remove(s);
        }
        System.out.println("7: " + cow);
    }
}

実行結果:

1: a-b
1: java.util.NoSuchElementException
2: java.lang.IllegalStateException
3: java.lang.IllegalStateException
4: java.lang.UnsupportedOperationException
5: java.lang.UnsupportedOperationException: remove
6: b
6: c
7: [a, c]

症状別の直し方

症状 理由 直し方
NoSuchElementException 1周の中で next() を2回呼び、要素数が奇数で足りなくなった next() の結果を変数に受けて、その変数を使い回す
要素が1つおきに処理される 条件判定と本処理でそれぞれ next() を呼んでいる 同上
ループが終わらない next() を呼び忘れ、位置が進まない while (it.hasNext()) の最初の行で next() を呼ぶ
IllegalStateException next() の前に remove()、または remove() を2回続けた next() 1回につき remove() は1回まで
UnsupportedOperationException List.of(...) や Arrays.asList(...) など、削除に対応していない List new ArrayList<>(元のList) でコピーしてから操作
ConcurrentModificationException 走査中に本体を直接変更した it.remove() / removeIf / 逆順 for / Stream に置き換え
2回目のループで何も出ない 使い終わったイテレータを使い回した iterator() を呼び直す

next() の呼び忘れについては、次の小さなコードで確かめました。hasNext() は位置を進めないため、何回呼んでも true のままです(無限ループを避けるため、5回で抜けるようにしています)。

import java.util.Iterator;
import java.util.List;

public class NoNext {
    public static void main(String[] args) {
        Iterator<String> it = List.of("a", "b").iterator();
        int guard = 0;
        while (it.hasNext()) {
            // next() を呼び忘れているので、位置が進まない
            if (++guard > 5) {
                System.out.println("5回まわっても hasNext() は " + it.hasNext() + " のまま");
                break;
            }
        }
    }
}
5回まわっても hasNext() は true のまま

直す前と後を並べると、ポイントは「next() は1周に1回、結果は変数に受ける」の1点に集約されます。

// 直す前:next() を2回呼んでいる
while (it.hasNext()) {
    if (it.next().startsWith("done:")) {
        System.out.println(it.next());   // 判定した要素の「次」が出てしまう
    }
}

// 直した後:1回だけ呼び、変数に受ける
while (it.hasNext()) {
    String t = it.next();
    if (t.startsWith("done:")) {
        System.out.println(t);
    }
}

実行結果の7番目にある CopyOnWriteArrayList は、変更のたびに内部の配列をコピーする List で、イテレータは作られた時点の内容を走査します。そのため走査中に本体を変更しても例外になりません。複数のスレッドから読み取りが多く行われる用途向けのクラスで、変更のたびにコピーが発生するため、単に「ループ中に削除したい」だけなら removeIf のほうが向いています。

よくある質問

Q. IteratorとIterableの違いは何ですか? A. Iterable は「順番に取り出せるもの」を表すインターフェースで、iterator() メソッドを1つ持ちます。Iterator はその iterator() が返すオブジェクトで、hasNext() と next() を使って実際に1つずつ取り出します。List や Set は Iterable、ループの進み具合を持つのが Iterator です。拡張 for 文で回せるのは Iterable(と配列)です。

Q. 拡張for文があるのに、Iteratorを直接使うのはどんなときですか? A. 走査しながら要素を削除したいとき(it.remove())、List を逆方向に走査したり置き換え・挿入をしたいとき(ListIterator)、自作クラスを拡張 for 文に対応させたいとき(Iterable の実装)の3つが代表的です。読むだけなら拡張 for 文がもっとも読みやすい書き方です。

Q. ConcurrentModificationExceptionはマルチスレッドでなくても起きますか? A. 起きます。1つのスレッドでも、イテレータで走査している最中にコレクション本体の add や remove を直接呼ぶと発生します。拡張 for 文や forEach の中での list.remove(...) が典型例です。

Q. removeIfとiterator.remove()はどちらを使えばよいですか? A. 条件に合う要素を削除するだけなら removeIf が短く、意図も伝わりやすい書き方です。削除する要素を別の List に移す、削除件数以外の集計をする、といった処理を同時に行いたいときは iterator.remove() が向いています。

まとめ

  • Iterator は hasNext() で確認し、next() で受け取る。位置を進めるのは next() だけで、使い終わったイテレータは使い回せない
  • 拡張 for 文の中で list.remove() を呼ぶと ConcurrentModificationException。スレッドが1つでも起き、例外は次の周回の next() で出る
  • 例外が出ないまま結果がずれる位置もある(後ろから2番目の削除、前からのインデックス削除)ため、書き方そのものを置き換える
  • ループ中の削除は removeIf が第一候補。別の処理も行うなら iterator.remove()、位置が必要なら逆順 for、元を残すなら Stream の filter
  • Iterable を実装して iterator() を返せば、自作クラスを拡張 for 文で回せる。走査位置は Iterator 側に持つ
  • ListIterator は双方向の走査と set / add ができる。Map は entrySet().iterator() で削除、setValue で書き換え
  • next() は1周に1回、結果は変数に受ける。これだけで NoSuchElementException や1つおきの処理を防げる

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java Iteratorの使い方|ループ中の削除とIterable実装」

株式会社RJCでは、Iteratorのようなコレクション操作の基礎を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

成長のヒミツ、 ぜんぶ丸わかり