Java 逆引き集 | Iterator.remove の正しい使い方 — ループ中の安全な削除

Java Java
スポンサーリンク

Iterator.remove の正しい使い方 — ループ中の安全な削除

拡張 for 文で走査中に remove すると例外になりがち。安全に要素を削除したいときは、Iterator を使い、next の直後に remove を呼ぶのが基本ルールです。初心者がつまずくポイントを、コード例とテンプレートで具体的に解説します。


基本ルールと前提

  • Iterator の3メソッド: hasNext(次があるか)、next(次を返す)、remove(直前に返した要素を削除)。
  • 正しい呼び方: next で要素を取得した「直後」に remove を呼ぶ。要素1つにつき remove は最大1回。
  • 間違いパターン:
    • 要素をまだ受け取っていないのに remove → IllegalStateException
    • 同じ要素に対して2回 remove → IllegalStateException
    • コレクションが remove 非対応 → UnsupportedOperationException
  • 拡張 for 文での削除は不可: 拡張 for は内部で Iterator を使うが、コレクション側へ直接 remove すると不整合で ConcurrentModificationException が起きることがある。安全な削除は Iterator.remove を使う。

正しい削除パターン(基本コード)

条件に合う要素だけ削除(List/Set共通)

import java.util.*;

public class IteratorRemoveBasic {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(List.of("A", "X", "B", "X", "C"));

        for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
            String s = it.next();          // 直前に返した要素が「現在の対象」
            if ("X".equals(s)) {
                it.remove();               // 安全に削除(list.remove(s) はダメ)
            }
        }
        System.out.println(list); // [A, B, C]
    }
}
Java
  • ポイント: 削除は必ず it.remove()。コレクションへ直接 list.remove(...) するのは不正。

よくある落とし穴と回避策

  • 拡張 for での削除(NG):
    • 原因: ループ中の構造変更で不整合が起きる。
    • 回避: Iterator を明示的に使って remove。あるいは removeIf を使う。
  • remove の呼び順ミス:
    • 原因: next を呼んでいない、または同要素に2回目の remove。
    • 回避: 「hasNext→next→条件→remove」の順を守る。要素ごと最大1回。
  • 非対応コレクション:
    • 原因: イミュータブル、または remove 非対応の Iterator(例: 一部のビュー)。
    • 回避: 例外を想定し、必要なら new ArrayList<>(...) でコピーして編集。
  • 外部からの並行変更:
    • 原因: 走査中に同じコレクションを別経路で add/remove。
    • 回避: 走査スレッドでのみ変更する設計、または並行コレクション/スナップショット利用。

実用レシピと代替手段

1. Set でも同様に安全に削除

Set<Integer> set = new HashSet<>(Set.of(1, 2, 99, 3));
for (Iterator<Integer> it = set.iterator(); it.hasNext(); ) {
    Integer v = it.next();
    if (v >= 90) it.remove();
}
System.out.println(set); // [1, 2, 3]
Java

2. ListIterator を使って「置換・挿入」もしたいとき

List<String> list = new ArrayList<>(List.of("a","x","b"));
ListIterator<String> it = list.listIterator();
while (it.hasNext()) {
    String s = it.next();
    if ("x".equals(s)) {
        it.set("y");      // 直前要素の置換(removeではなく置換)
        it.add("z");      // 現在位置に挿入(この add 後は set は呼べない規約)
    }
}
System.out.println(list); // [a, y, z, b]
Java

3. removeIf でシンプルに(内部で安全な削除)

List<String> list = new ArrayList<>(List.of("A","X","B","X","C"));
list.removeIf("X"::equals); // 条件に合う要素を一括削除
System.out.println(list); // [A, B, C]
Java
  • ポイント: Java 8+ の定番。簡潔で安全。Iterator.remove のラッパ的存在。

4. 並行コレクションでの削除(弱一貫性)

import java.util.concurrent.ConcurrentLinkedQueue;

ConcurrentLinkedQueue<String> q = new ConcurrentLinkedQueue<>();
q.add("A"); q.add("X"); q.add("B");

for (Iterator<String> it = q.iterator(); it.hasNext(); ) {
    String s = it.next();
    if ("X".equals(s)) it.remove(); // 並行キューは iterator の remove が反映される
}
Java
  • 注意: 並行コレクションはイテレーションが「弱一貫性」。全変更が完全には見えない可能性があるため、ドレインや removeIf など用途に合わせた設計を。

例題で身につける

例題1: 偶数だけを安全に除去(List)

List<Integer> nums = new ArrayList<>(List.of(1,2,3,4,5,6));
for (Iterator<Integer> it = nums.iterator(); it.hasNext(); ) {
    int x = it.next();
    if (x % 2 == 0) it.remove();
}
System.out.println(nums); // [1, 3, 5]
Java

例題2: 指定タグを全削除(Set)

Set<String> tags = new HashSet<>(Set.of("java","x","stream","x"));
for (Iterator<String> it = tags.iterator(); it.hasNext(); ) {
    String t = it.next();
    if ("x".equals(t)) it.remove();
}
System.out.println(tags); // [java, stream]
Java

例題3: 条件で置換や挿入が必要(ListIterator)

List<String> words = new ArrayList<>(List.of("red","old","blue"));
ListIterator<String> it = words.listIterator();
while (it.hasNext()) {
    String w = it.next();
    if ("old".equals(w)) {
        it.set("new");   // 置換
        it.add("bright"); // 置換直後に挿入(位置は "new" の後)
    }
}
System.out.println(words); // [red, new, bright, blue]
Java

テンプレート集(そのまま使える)

  • Iterator で安全削除(定型)
for (Iterator<T> it = list.iterator(); it.hasNext(); ) {
    T e = it.next();
    if (shouldRemove(e)) it.remove();
}
Java
  • removeIf(最短)
list.removeIf(this::shouldRemove);
set.removeIf(this::shouldRemove);
Java
  • **ListIterator(置換・挿入も)
for (ListIterator<T> it = list.listIterator(); it.hasNext(); ) {
    T e = it.next();
    if (cond(e)) { it.set(modify(e)); it.add(extra()); }
}
Java
  • 拡張 for 文で「編集しない」場合のみ
for (T e : list) {
    // 読むだけ/集計だけにする。編集はしない。
}
Java

実務でのコツ

  • 編集が必要なら Iterator/ListIterator、簡単なら removeIf: 拡張 for は「読み取り専用」のつもりで。
  • 例外に備える: サブリストやイミュータブルビューは remove 非対応のことがある。必要なら実体へコピーして編集。
  • 並行処理時は収集の仕方を再考: 共有コレクションを並列で編集せず、まずフィルタした結果を別構造に集めるほうが安全。
  • ストリームの考え方: 削除の代わりに filtercollect で「残す側」を作るのも定番。編集より宣言的に書けて安全。

まとめ

  • Iterator.remove は「next 直後に一度だけ」が鉄則。拡張 for での直接削除は避け、必ず Iterator 経由で変更する。
  • 置換や挿入が必要なら ListIterator、簡潔に済ませたいなら removeIf。
  • 並行・ビュー・イミュータブルなどの特殊ケースを意識し、例外や一貫性に注意しながら安全な削除を設計しよう。
タイトルとURLをコピーしました