List を走査する

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

このレッスンで分かること

  • 拡張 for ループ for (E e : list) が現代 Java の基本
  • インデックスが必要なら従来 for、要素変更が必要なら ListIterator
  • ループ中に remove add すると ConcurrentModificationException
  • forEach(e -> ...) で関数型スタイルでも書ける (副作用注意)

List を走査する とは

ArrayList の中身を最初から最後まで順番に取り出す。インデックス for、拡張 for、Iterator の 3 つの走査方法を使い分けられるようになる。

ArrayList の中身を順番に取り出す

配列の次に Java で必ず出てくるのが ArrayList です。ArrayListjava.util パッケージに入っているクラスで、配列と違って要素を後から自由に追加・削除できる「伸び縮みするリスト」です。実務の Java では、固定長の配列 (int[] String[]) よりも ArrayList を使う場面が圧倒的に多く、List を走査して合計や平均を求める処理は毎日のように書きます。

ArrayList は内部的には配列を持っていて、足りなくなったら自動で大きい配列を作り直してくれる仕組みです。だから配列の高速さと、可変長の便利さを両立できます。Java を書く上で最も使うコレクションのひとつです。

このレッスンでは、ArrayList<Integer> に整数を入れて、すべての要素を順番に取り出して合計するというパターンを学びます。for 文の書き方は 1 種類ではなく、Java では大きく分けて 3 種類の走査方法があります。それぞれの違いをきちんと理解して、場面に応じて使い分けられるようになりましょう。

走査方法その 1: インデックス for

もっとも基本的なのが「インデックスを 0 から size() 未満まで回す」やり方です。配列を回すときと同じ感覚で書けるので、最初に覚えるならこの形が分かりやすいでしょう。

Java

ArrayList<Integer> list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); int sum = 0; for (int i = 0; i < list.size(); i++) { sum += list.get(i); } // sum は 6 になる

ここでのポイントは次のとおりです。

  • list.size() は要素数を返すメソッド。配列の length (プロパティ) と違って メソッド呼び出し なので末尾に () が必要
  • list.get(i)i 番目の要素を取り出す。配列の arr[i] とは書き方が違うことに注意
  • インデックスは 0 始まり、最後は list.size() - 1

インデックス for は「何番目の要素か」を i という変数で持てるのが強みです。「3 番目だけ別処理にしたい」「奇数番目だけ集計したい」のような、位置情報を使う処理にはこの書き方が向いています。

走査方法その 2: 拡張 for (for-each)

もっとシンプルに、ただ全要素を頭から順に処理したいだけなら、拡張 for (for-each 文) を使うのが Java では一番きれいです。Java 5 から導入された書き方で、現代の Java コードではほぼ標準と言ってもいい構文です。

Java

ArrayList<Integer> list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); int sum = 0; for (int n : list) { sum += n; } // sum は 6 になる

for (int n : list): は「list の中の各要素を n という名前で取り出す」という意味です。読みは「フォー イーチ エヌ イン リスト」。インデックスを書く必要がなく、list.get(i) のような呼び出しもいらないので、目的が「全部回す」だけならコードがぐっと短く読みやすくなります。

このレッスンの課題でもこの拡張 for を使います。中で何が起きているかを図にすると、次のような流れで動いています。

diagram (will load when visible)

反復のたびに「次の要素はある?」と内部で確認し、あれば n に取り出して処理、なくなったらループを抜ける。インデックス管理がなくなった分、処理本体の意図が読みやすくなっているのが分かるはずです。

拡張 for は「インデックスは要らない、全要素に同じ処理をしたい」場面で使います。Java 実務コードの 7〜8 割はこのパターンと言っても過言ではありません。迷ったらまず拡張 for を選びましょう。

走査方法その 3: Iterator

もう 1 つ、Iterator というオブジェクトを使う書き方もあります。これは拡張 for の中身そのものです。list.iterator() で取り出した Iterator に対して hasNext() で「まだ次があるか」を聞き、next() で次の要素を取り出します。

Java

Iterator<Integer> it = list.iterator(); while (it.hasNext()) { int n = it.next(); sum += n; }

普段の集計処理で Iterator を直接書くことは少ないですが、「走査しながら条件に合う要素を削除したい」場面では it.remove() を使うために手書きが必要になります。詳しくは後の章で扱うので、ここでは「拡張 for は内部で Iterator を使っているんだな」程度に押さえておけば十分です。

ConcurrentModificationException に気をつける

初心者が一度はハマるエラーに ConcurrentModificationException というものがあります。これは「走査中の List を、別の経路から書き換えた」ときに飛ぶ例外です。

Java

for (int n : list) { if (n == 0) list.remove(Integer.valueOf(n)); // NG: 走査中に remove }

拡張 for や Iterator で走査している最中に list.remove(...) を呼ぶと、内部で「あれ? さっきと要素数が変わってる」と検知され、例外が飛んで処理が止まります。これを避けるには、Iterator を自分で取り出して it.remove() を使うか、削除対象を別の List に貯めておいてループ後にまとめて消すという書き方を使います。

エラー名は長いですが、意味は「走査中に並行して変更が入ったよ」。Concurrent (同時に) + Modification (変更) + Exception (例外) と分解すれば覚えやすいはずです。

よくある間違い

ArrayList を扱い始めた頃に多い失敗を 3 つまとめておきます。

  • list.size と書いてしまう — 要素数は list.size() です。配列の arr.length はカッコなしのフィールドですが、ArrayListsize()メソッド なので必ず () が必要です。list.size と書くとコンパイルエラーになります
  • intInteger を取り違えるArrayList<Integer> には基本型の int をそのまま入れているように見えますが、実はラッパクラス Integer に自動変換 (auto-boxing) されています。list.remove(5) と書くと「インデックス 5 番目を削除」、list.remove(Integer.valueOf(5)) と書くと「値が 5 の要素を削除」になり、結果がまったく違うので注意してください
  • 走査中に remove する — 上で書いた ConcurrentModificationException の話です。拡張 for ループ中に list.remove(...) を呼ぶと例外が飛びます。安全に消すなら Iterator.remove() を使うか、別 List に集めてからまとめて削除します

もう 1 つ、見落としがちなのが「空の ArrayList を走査しても何も起きない」という挙動です。for (int n : list) は要素がなければ中身を一度も実行せずに終わるだけで、例外は飛びません。これは便利な反面、「空リストのテストケースを書いていなかったので合計が 0 で返ってバグに気付かなかった」というケースが現場でよくあります。空リストの結果がどうあるべきかは、最初に設計で決めておくと安全です。

「面倒だな」と感じる細かいルールほど、後でバグの温床になります。size()()Integerint の違い、走査中 remove の禁止。この 3 つは Java の ArrayList を触る限り永遠についてくる注意点です。

実務で遭遇するパターン

中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。

  • 集計ループint sum = 0; for (Integer n : list) sum += n; の単純集計
  • Map.entrySet() の走査for (Map.Entry<K,V> e : map.entrySet()) ... で Key と Value をペアで処理
  • フィルタしながら新リストを作る ─ Stream の filter で済むことも多いが、複雑な条件分岐なら for のほうが読みやすい
  • バッチで条件付き集計 ─ 1 つのループで合計 / 件数 / 最大値を同時に取る

コードレビューで指摘されがちなポイント

PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。

  • for (int i = 0; i < list.size(); i++)get(i)LinkedList で呼ぶ ─ O(n²)、enhanced for を使う
  • 走査中に list.remove(...)ConcurrentModificationExceptionIterator.remove() か新リストを作る
  • Stream で十分なのに for ─ シンプルな map filter count は Stream のほうが意図が明確
  • forEach 内で外部変数を変更int[] sum = {0} のようなハック。reduce を使うべき

パフォーマンス考慮事項

  • ArrayListenhanced for と従来 for(i) はほぼ同等 ─ 配列アクセスに最適化される
  • LinkedListfor(i) は致命的get(i) が毎回 O(n)、合計 O(n²)
  • Iterator のオーバーヘッド ─ 数 ns 程度、無視できる
  • forEach ラムダはホットパスで僅かに遅い ─ ベンチで差が出ることもあるが、99% は気にしない
この章のポイント

ここまでの要点 走査は enhanced for か Stream が基本。get(i) の従来 for は LinkedList で罠。走査中の変更は Iterator.remove または別リストを作る。

やってみよう

それでは実際に手を動かして、sumList() メソッドを完成させましょう。

  1. ArrayList<Integer> を新しく作る (new ArrayList<>())
  2. list.add(5) list.add(10) list.add(15) list.add(20) で 4 つの値を追加する
  3. int sum = 0; を用意して、拡張 for で全要素を sum に足していく
  4. 最後に return sum; で合計を返す

期待される戻り値は 50 (= 5 + 10 + 15 + 20) です。

慣れてきたら同じ処理をインデックス for や Iterator でも書き直してみてください。書き方は 3 通りでも結果は同じになるはずです。3 つの構文が手に馴染んだら、ArrayList の世界の入り口はもうクリアです。次のレッスンでは、List に対して条件で絞り込んだり、要素を変換したりするパターンを学んでいきましょう。

よくある質問

Q. 中級の内容は実務でどれくらい使いますか?

A. Collection(List/Map/Set)、Stream、例外処理、ジェネリクスは毎日のように登場します。Date/Time API、Files、try-with-resources も実務で頻出するため、本コースの内容は実プロジェクトでそのまま役立ちます。

Q. Stream と for ループはどっちで書くべき?

A. 可読性で選んでください。filter + map + collect が綺麗にハマるなら Stream、副作用や複雑な分岐が多いなら for ループの方が読みやすいです。一律にどちらを使うべきという正解はなく、チームのコーディング規約に合わせるのが現実的です。

Q. 次のステップでは何を学ぶべきですか?

A. 中級の基礎が固まったら、Spring Boot で Web API を作る、JUnit でテストを書く、Maven/Gradle でビルドを管理する、といった実プロジェクトのスキルに進むと効果的です。OSS のコードを読む経験も大きく成長を促します。

次のレッスン

次は 要素の削除と検索 で、要素の削除と検索 を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. List 走査 の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. List 走査 とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. メソッド内で ArrayList<Integer> を新しく作り、5, 10, 15, 20 の 4 つの値をこの順番で add すること
  2. 全要素の合計は 拡張 for (for (int n : list)) を使って計算すること
  3. 戻り値の型は int、戻り値は 50 になること

入出力例

test-cases.txt

sumList()50

ヒント

main.java
main.java
学習モード

メモ

List を走査する

⌘S で保存