Java | Java 標準ライブラリ:for-each の内部動作

Java Java
スポンサーリンク

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 の中身を変えない
}
Java

n はただのローカル変数なので、配列の中身には影響しません。

中身を変えたいならインデックスが必要なので、普通の 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 を直接いじっている
    }
}
Java

Iterator は「自分が想定していないところからコレクションを変更された」と気づくと、
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();  // 直前に返した要素を安全に削除
    }
}
Java

Iterator 自身がコレクションの内部状態と整合を取りながら削除してくれるので、
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 文に切り替える

タイトルとURLをコピーしました