for-each 文をまず感覚でつかむ
Java の for-each(拡張 for 文)は、こういう書き方のやつです。
for (String name : names) {
System.out.println(name);
}
Java「names の中身を先頭から順番に 1 個ずつ取り出して、name に入れて処理してね」
という“超略記”です。
でも、Java 仮想マシンは「for-each なんて知らない」ので、
コンパイラがこれを普通の for 文や Iterator を使ったコードに“展開”してくれています。
for-each の「内部動作」を理解すると、
なぜ配列にもコレクションにも同じ書き方ができるのか
なぜ for-each 中に要素削除すると ConcurrentModificationException が出るのか
といった疑問が、一気にスッキリします。
配列に対する for-each は「インデックス for 文」に展開される
コード上はシンプル、裏では普通の for
配列に対して for-each を書く場合:
int[] nums = {10, 20, 30};
for (int n : nums) {
System.out.println(n);
}
Javaコンパイラは、ざっくり次のようなコードに変換してくれます(イメージです)。
int[] tmp = nums;
int len = tmp.length;
for (int i = 0; i < len; i++) {
int n = tmp[i];
System.out.println(n);
}
Javaつまり、やっていることは「ただのインデックス for 文」です。
for (要素型 変数 : 配列)
は、「配列の 0 から length-1 まで順にアクセスして変数に代入」という処理に変換されます。
だから「配列の中身の変更」は普通にできる
配列の場合、for-each の変数は要素の値のコピーです。
for (int n : nums) {
n = n * 2; // これは nums の中身を変えない
}
Javan はただのローカル変数なので、配列の中身には影響しません。
中身を変えたいならインデックスが必要なので、普通の for 文で書く必要があります。
for (int i = 0; i < nums.length; i++) {
nums[i] = nums[i] * 2;
}
Java「for-each で配列の値を直接変えられない」のは、
内部的に「値をコピーしてから使っている」だけだから、という理解で OK です。
コレクションに対する for-each は「Iterator ループ」に展開される
List / Set などは Iterator を裏で使っている
List<String> などのコレクションに対する for-each:
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Carol");
for (String name : names) {
System.out.println(name);
}
Javaこれはコンパイラが、だいたい次のようなコードに変換します(イメージ)。
for (Iterator<String> it = names.iterator(); it.hasNext(); ) {
String name = it.next();
System.out.println(name);
}
Javaここで重要なのは、
for-each(コレクション版)は
= iterator() でイテレータを取得してhasNext() でループ判定してnext() で順番に要素を取り出す
という、Iterator ベースのループに変換されるということです。
なぜ for-each できるコレクションとできないクラスがあるのか
for-each の対象にできるのは、
配列Iterable インターフェースを実装しているクラス
です。
Iterable には
Iterator<E> iterator();
Javaというメソッドが定義されています。
「iterator() を実装しているからこそ、
for-each の裏側で Iterator ループを書ける」
という関係です。
逆に、iterator() を持たない自作クラスは、そのままでは for-each の対象にはできません。
for-each 中に要素削除すると怒られる理由(ここが実務的に超重要)
よくある NG コード
「条件に合う要素を削除したい」という場面で、初心者がやりがちなのがこれです。
for (String name : names) {
if (name.startsWith("A")) {
names.remove(name); // ほぼ確実に ConcurrentModificationException
}
}
Java実行すると、多くの場合 ConcurrentModificationException が出ます。
なぜかというと、裏でこう書いているのと同じだからです。
for (Iterator<String> it = names.iterator(); it.hasNext(); ) {
String name = it.next();
if (name.startsWith("A")) {
names.remove(name); // Iterator ではなく List を直接いじっている
}
}
JavaIterator は「自分が想定していないところからコレクションを変更された」と気づくと、
fail-fast(すぐに例外)を起こします。
正しい書き方:Iterator の remove を使う
ループ中に安全に削除したい場合は、
for-each ではなく Iterator を明示的に使って、it.remove() を呼びます。
Iterator<String> it = names.iterator();
while (it.hasNext()) {
String name = it.next();
if (name.startsWith("A")) {
it.remove(); // 直前に返した要素を安全に削除
}
}
JavaIterator 自身がコレクションの内部状態と整合を取りながら削除してくれるので、ConcurrentModificationException を避けられます。
「for-each 中に list.remove() するのはダメ」
「削除したければ Iterator を使う」
これは for-each の内部動作を知っているかどうかで、
実務のバグ率が大きく変わるポイントです。
for-each をいつ使って、いつ使わないか
使うべき場面
for-each が向いているのは、
インデックスが特に必要ない
要素を順番に読むだけでよい
ループ中にコレクションの構造(要素数・位置)を変えない
という場面です。
例えば:
for (String name : names) {
System.out.println(name);
}
Javaや
for (Integer id : idSet) {
// 集計だけする
}
Javaのような、「読むだけ」の処理には最高に相性が良いです。
避けた方がいい場面
逆に、
要素を削除・挿入したい
インデックスが必要(前後要素を見たい、何番目か知りたい)
コレクションを書き換えながらループしたい
といった場面では、for-each は向きません。
そのときは、
配列なら普通の for 文(インデックス付き)
List なら for (int i = 0; i < list.size(); i++) や Iterator
などに切り替えるべきです。
まとめ:for-each の内部動作を自分の中でこう整理する
for-each 文を初心者向けに一言で整理すると、
配列に対しては「インデックス for 文」に
コレクションに対しては「Iterator ループ」に
コンパイラが自動展開してくれる糖衣構文です。
特に意識しておきたいのは次のようなポイントです。
配列版 for-each は、値のコピーを変数に渡しているだけなので、要素の書き換えには向かない
コレクション版 for-each は Iterator を裏で使っているので、ループ中に直接 remove すると例外になる
「読むだけ」の処理なら for-each が一番シンプルで安全
「削除を伴う処理」「インデックスが欲しい処理」では、素直に Iterator や普通の for 文に切り替える
