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]
Java2. 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]
Java3. 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 非対応のことがある。必要なら実体へコピーして編集。
- 並行処理時は収集の仕方を再考: 共有コレクションを並列で編集せず、まずフィルタした結果を別構造に集めるほうが安全。
- ストリームの考え方: 削除の代わりに
filter→collectで「残す側」を作るのも定番。編集より宣言的に書けて安全。
まとめ
- Iterator.remove は「next 直後に一度だけ」が鉄則。拡張 for での直接削除は避け、必ず Iterator 経由で変更する。
- 置換や挿入が必要なら ListIterator、簡潔に済ませたいなら removeIf。
- 並行・ビュー・イミュータブルなどの特殊ケースを意識し、例外や一貫性に注意しながら安全な削除を設計しよう。
