- JAVA
Java Iteratorの使い方|ループ中の削除とIterable実装
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期連続の黒字決算。