インデックスがただ邪魔になるとき
カウンタ付きの for ループは、カウンタ・条件・更新ステップを与えてくれます。しかし実際には、要素の位置などまったく気にしない場面がとても多くあります。先頭から末尾まで、順番に各要素へ何かをしたいだけなのです。そのためにインデックスを管理するのは無駄な手間で、まさにそこに off-by-one(1 ずれ)バグが忍び込みます。
for-each ループ(Java での呼び名は 拡張 for)はカウンタを完全に取り除きます。変数に名前を付け、それをコレクションに向ければ、ループが各要素を順番に手渡してくれます。
基本構文
形は for (型 element : collection) です。コロンは「の中の」という言葉として読みましょう:
i も scores.length も scores[i] もありません。各回ごとに score は次の要素そのものです。ループは要素ごとに 1 回実行され、要素がなくなると自動的に停止します。末尾を越えて走ることも、1 要素早く始めることもできません。
リストをループする
同じループは、反復可能なものすべてに対して機能します。これには List、Set、その他のコレクション型が含まれます。要素の型は変数名の前に置きます:
langs の中身が配列なのか連結リストなのか、それとも他の何かなのかを知ったり気にしたりする必要がなかった点に注目してください。for-each はそのすべてで同じように動きます。これこそが本当の強みです。どんなコレクションにも 1 つの読みやすい構文で済むのです。
var で型名を書かずに済む
要素の型が長かったり自明だったりする場合、var を使えばコンパイラに型を推論させ、繰り返しを省けます:
var がなければ、そのループ変数は長ったらしい Map.Entry<String, Integer> になります。var は読みやすさを保ちつつ、型はコンパイル時に完全にチェックされます。ゆるい動的型ではありません。
変更の落とし穴
ここが誰もがはまるルールです。for-each ループがコレクションをたどっている最中に、そのコレクションへ要素を追加したり削除したりはできません。やってしまうと ConcurrentModificationException が投げられます:
List<String> items = new ArrayList<>(List.of("a", "b", "c"));
for (String item : items) {
if (item.equals("b")) {
items.remove(item); // ConcurrentModificationException を投げる
}
}
ループは下でリストが変わったことに気づき、要素を黙って飛ばしたり繰り返したりするのではなく、処理を打ち切ります。安全に削除するには、ループが認識している remove() を持つ明示的な Iterator まで降りてください:
よく使われる近道は items.removeIf(item -> item.equals("b")) で、同じことを 1 行で行います。
読み取り専用、再代入はできない
もう 1 つの微妙な制約:ループ変数への代入はローカルのコピーを変えるだけで、コレクションは変わりません。これは、ループ変数が生きた参照である言語から来た人を驚かせます:
配列に書き戻す必要があるなら、インデックスが必要で、それはつまり従来のカウンタ付き for ループです:for (int i = 0; i < nums.length; i++) nums[i] = nums[i] * 10;。オブジェクトの要素については、変数が指すオブジェクトを(たとえばセッターを呼んで)変更することはできますが、コレクション内でそれ自体を別のものに置き換えることはできません。
break と continue はそのまま使える
for-each は本物のループなので、break と continue は他の場所とまったく同じように振る舞います。break はループを抜け、continue は次の要素へ飛びます:
これは keep を、続いて keep を出力します。"skip" を飛ばし、"never" に達する前に "stop" で止まります。つまり、すべての要素を訪れることに縛られているわけではありません。よりすっきりしたコードと引き換えにインデックスを手放すだけなのです。
次へ:配列
これまで配列が実際には何なのかを深く考えずに、何度かループしてきました。配列とは、あの .length フィールドを持つ、固定サイズでインデックス付きの入れ物です。次のページでは最初に戻り、配列をきちんと扱います。宣言の仕方、最初に入っているデフォルト値、そしてその固定サイズが伸縮できる ArrayList とどう違うのかを取り上げます。
よくある質問
Java の for-each ループとは何ですか?
for-each ループ(拡張 for とも呼ばれます)は、カウンタなしで配列やコレクションのすべての要素をたどります:for (型 item : collection) { ... }。「コレクションの各 item について」と読めます。要素そのものだけが必要でインデックスは一切不要なとき、カウンタ付きの for ループよりすっきりします。
Java の for ループと for-each ループの違いは何ですか?
従来の for ループは明示的なカウンタ(for (int i = 0; i < arr.length; i++))を使うため、インデックスと方向を自分で制御できます。for-each ループ for (型 x : arr) にはインデックスがなく、すべての要素を順番に訪れます。読み取り専用で先頭から末尾までたどる場合は for-each を、インデックスが必要なとき、要素を飛ばしたいとき、コレクションの構造を変更したいときはカウンタ付きループを使いましょう。
なぜ for-each ループが ConcurrentModificationException を投げるのですか?
for-each で反復している最中に、コレクションに対して add() や remove() を呼んだからです。ループは構造的な変更を検知し、未定義動作からあなたを守るために例外を投げます。要素を安全に削除するには、明示的な Iterator とその remove() メソッドを使うか、削除したい要素を集めておいてループの後で取り除きます。