ArrayList を使う

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

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

  • ArrayList<E> は可変長配列。add で末尾追加、size() get(i) remove(i) が基本
  • ジェネリクスはプリミティブ NG ─ int ではなく Integer を使う (オートボクシング)
  • import java.util.ArrayList; を忘れずに。raw 型new ArrayList() は禁じ手
  • list.toString()[1, 2, 3] 形式の文字列を返す

ArrayList を使う とは

Java の代表的な可変長リスト ArrayList の使い方を学び、add で要素を追加して toString で中身を取り出すコードを書いてみよう。

配列の不便さと ArrayList の登場

Java 入門コースで使ってきた配列 (int[]String[]) は、書き方がシンプルで for ループとの相性も良い反面、ひとつ大きな弱点があります。それは サイズを後から変えられない という制約です。たとえば new int[3] と書いた瞬間に「箱は 3 つ」と決まってしまい、4 つ目の値を入れたくなったら新しい配列を作って中身をコピーし直す、という面倒な手順を踏むことになります。

配列はメモリ上に「連続した固定サイズの箱」をドンと確保する仕組みです。実行中に箱の数を変えられないのは、CPU やメモリレベルでは合理的な設計ですが、業務アプリで「ユーザーが何件入れてくるか分からない」というデータを扱うときには不便すぎます。

そこで登場するのが java.util.ArrayList です。ArrayList は、見た目は配列に似ていますが、add で末尾に要素を足したり remove で消したりするとサイズが自動で伸縮してくれる「可変長配列」です。中身では大きめの配列を持っていて、足りなくなったら勝手にもっと大きな配列を確保して引っ越してくれます。利用者はその面倒事を意識しなくて済みます。

diagram (will load when visible)

業務システムでも Android アプリでも、ユーザー入力や API レスポンスのリストを保持するときに最初に検討するのが ArrayList です。本章ではまずこの基本の使い方を、配列との違いを意識しながら身につけていきましょう。

import java.util.ArrayList; を忘れない

ArrayListStringint と違って、いきなり名前を書いただけでは使えません。java.util パッケージというところに入っているクラスなので、ファイルの先頭で import してから使う約束です。

Java

import java.util.ArrayList; public class Solution { public static String buildList() { ArrayList<Integer> list = new ArrayList<>(); list.add(10); list.add(20); list.add(30); return list.toString(); } }

冒頭の import java.util.ArrayList; がなくても、java.util.ArrayList<Integer> list = ... のようにフルパスで書けば動きますが、長くて読みにくいので import するのが普通です。IntelliJ IDEAVS Code などの IDE を使うと、ArrayList と打った瞬間に自動で import を追加してくれます。

Java の import は「このクラスをこのファイル内で短い名前で呼べるようにする」という宣言です。コードを実行時に「読み込む」わけではないので、Pythonimport とは少し意味が違うことに注意してください。

ジェネリクス <Integer> で型を縛る

ArrayList の宣言で目を引くのが、<Integer> という山かっこの部分です。これは ジェネリクス と呼ばれる仕組みで、「このリストには Integer 型しか入れない」とコンパイラに宣言しています。

Java

ArrayList<Integer> numbers = new ArrayList<>(); ArrayList<String> names = new ArrayList<>(); ArrayList<Double> prices = new ArrayList<>();

ここで気をつけたいのが、ジェネリクスには intdouble などの プリミティブ型 をそのまま入れられないことです。整数を入れたいときは Integer、小数なら Double、真偽値なら Boolean という、対応する ラッパー型 を指定します。intInteger の橋渡しは Java が自動でやってくれる (オートボクシング) ので、list.add(10) のように普通の数値を渡せば内部で Integer.valueOf(10) に変換されます。

new ArrayList<>() の右側のかっこが空っぽなのは、ダイヤモンド演算子 という Java 7 以降の省略記法で、「左側と同じ型を使うよ」という意味です。古いコードでは new ArrayList<Integer>() のように両側に型を書いていますが、現代の Java では空のダイヤモンドが標準です。

ジェネリクスを使わずに ArrayList list = new ArrayList(); と書くこともでき、これを raw 型 と呼びます。動くには動きますが、Object として何でも入る代わりに取り出すときにキャストが必要で、コンパイラの警告も出ます。現代の Java では raw 型は絶対に使わない、と覚えてください。

よく使う基本メソッド

ArrayList には便利なメソッドがたくさんありますが、最初に覚えるべきは次の 4 つです。

  • add(value) — 末尾に要素を追加する。list.add(10); のように呼ぶ
  • get(index) — 指定したインデックスの要素を取り出す。list.get(0) で先頭
  • size() — 現在入っている要素数を返す。配列の .length に相当
  • remove(index) — 指定インデックスの要素を削除して、後ろの要素を詰める

コードで見るとこんなイメージです。

Java

import java.util.ArrayList; public class Demo { public static void main(String[] args) { ArrayList<String> tasks = new ArrayList<>(); tasks.add("買い物"); tasks.add("洗濯"); tasks.add("勉強"); System.out.println(tasks.size()); // 3 System.out.println(tasks.get(0)); // 買い物 tasks.remove(1); // 洗濯を消す System.out.println(tasks); // [買い物, 勉強] } }

最後の System.out.println(tasks);[買い物, 勉強] と表示されているのに注目してください。これは ArrayListtoString() が自動で呼ばれた結果で、[] で囲まれ、要素は , (カンマ + 半角スペース) で区切られます。今回の課題ではこの形式の文字列を return するので、必ず list.toString() を返すようにしてください。

diagram (will load when visible)

配列との対応表

配列と ArrayList は似ているようで違うものが多いので、よく使う操作を対応表で並べてみます。

やりたいこと配列 (int[])ArrayList<Integer>
作成new int[3]new ArrayList<>()
末尾追加できない (作り直し)list.add(10)
取り出しarr[0]list.get(0)
要素数arr.lengthlist.size()
削除できない (作り直し)list.remove(0)
文字列化Arrays.toString(arr)list.toString()

arr[0] のような [] の記法は配列専用で、ArrayList には使えないことに気をつけてください。list[0] と書くとコンパイルエラーになります。

よくある間違い

ここで、初学者がほぼ全員ハマる落とし穴を 3 つ紹介します。事前に知っておくだけで、エラーで悩む時間がぐっと減ります。

  • import を忘れるArrayList を使うには import java.util.ArrayList; が必要です。これを忘れると cannot find symbol というエラーが出ます。IDE なら自動補完でやってくれますが、手書きの試験などでは注意しましょう
  • 要素アクセスで list[0] と書いてしまうArrayList はクラスなので、配列の [] 記法は使えません。必ず list.get(0) のようにメソッド呼び出しで取り出します。逆に、取り出した値の型 (Integer) を int 変数に入れるときは int x = list.get(0); のように自動で変換されます (オートアンボクシング)
  • raw 型で書いてキャストが必要になるArrayList list = new ArrayList(); のようにジェネリクスを省くと、get(0) の戻り値は Object になります。Integer x = (Integer) list.get(0); のように明示的なキャストが必要になり、安全性も下がります。型は必ず <> で指定しましょう

もう 1 つよくあるのは、ArrayList<int> と書こうとしてエラーになるパターンです。前述のとおり、ジェネリクスにはプリミティブ型は入れられないので、ArrayList<Integer> のようにラッパー型を使います。これは Java の文法上のルールなので、暗記してしまうのが早いです。

エラーメッセージ unexpected type required: reference found: int などが出たら、ほぼ間違いなく ArrayList<int> 系のミスです。落ち着いて Integer に直しましょう。

実務で遭遇するパターン

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

  • API レスポンスの整形 ─ DB から取った行をループで ArrayList<UserDto> に詰めてサーバから返す、典型的なパターン
  • ユーザー入力の一時バッファ ─ ファイルアップロード時の行や、検索条件のタグなど数が決まらない入力を保持する
  • Collectors.toList() ─ Stream の終端で ArrayList 相当を作る。Stream.toList() は Java 16+ で不変リスト
  • ページネーションの中間結果 ─ 100 件取って 1 ページ分だけスライスして返す、というロジックでよく使う

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

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

  • List<Integer> で宣言したほうがいい ─ 変数宣言は具象型 ArrayList ではなくインタフェース List にするのが定石
  • 初期サイズを指定していない ─ 容量が予測できる場合は new ArrayList<>(1000) で確保し、再アロケートを防ぐ
  • for(int i; ...; i++)list.size() を毎回呼んでいる ─ コンパイラは多くの場合最適化するが、可読性で enhanced for を勧められる
  • Arrays.asList(...) を可変だと思っている ─ 戻り値は固定長で add できない。new ArrayList<>(Arrays.asList(...)) でくるむ

パフォーマンス考慮事項

  • add(value) は平均 O(1)、たまに O(n) ─ 内部配列が満杯になると 1.5 倍に拡張してコピーする
  • remove(index) は O(n) ─ 後ろを詰めるので、先頭削除を多用するなら LinkedListArrayDeque を検討
  • contains(value) は O(n) ─ 重複チェックを頻繁にやるなら HashSet のほうが速い
  • get(index) は O(1) ─ 配列ベースなので、LinkedList の O(n) より圧倒的に速い
この章のポイント

ここまでの要点 ArrayList<E> で可変長コレクション、ラッパー型を使い、宣言は List<E>。サイズが予測できるなら初期容量を指定する。

やってみよう

それでは今回の課題に挑戦してみましょう。やることは次のとおりです。

  1. ファイル先頭で import java.util.ArrayList; を書く
  2. Solution.buildList(int a, int b, int c) の中で ArrayList<Integer>new で作る
  3. 引数 a b c順番で add し、3 つの要素を末尾に追加する
  4. 最後に return list.toString();ArrayList の標準フォーマットを返す

テストでは戻り値の文字列を比較します。buildList(10, 20, 30) なら "[10, 20, 30]" (角かっこ + カンマ + 半角スペース) になります。add 順を入れ替えると結果の並びも入れ替わるので、引数を受け取った順に add することが重要です。同じ値を重複して入れると、リストの size() はそのぶん増えて "[5, 5, 5]" のように同じ要素が並びます。

慣れてきたら、list.add(0, 99); のようにインデックスを指定して途中に挿入したり、list.remove(1) で 2 番目を消したりして size() の変化を観察してみてください。次のレッスンでは、この ArrayListfor-each で回したり、Collections.sort で並べ替えたりと、もう一歩実践的な使い方に踏み込んでいきます。

よくある質問

Q. ArrayList と LinkedList はどう使い分けますか?

A. ほとんどの場面で ArrayList が高速です。LinkedList は先頭への追加・削除が O(1) ですが、要素の参照が O(n) になるため意外と遅くなります。実際の業務コードでは LinkedList を使うケースは稀で、迷ったら ArrayList を選んでください。

Q. ArrayList の初期容量は指定すべきですか?

A. 件数が事前に分かるなら new ArrayList<>(1000) のように指定すると内部配列の再割り当てを防げます。100 万件レベルでは性能差が顕著です。サイズが分からないときはデフォルト(10)のままで OK で、AutoResizing に任せます。

Q. ループ中に remove するとなぜ例外になりますか?

A. for-each の内側で remove すると ConcurrentModificationException が出ます。Iterator.remove() を使うか、removeIf(p -> ...) のように述語で一括削除するのが安全です。インデックスループで後ろから消すのも昔ながらの定石です。

次のレッスン

次は List を走査する で、List を走査する を学びます。

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

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

理解度チェック (30 秒)

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

この章のポイント

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

関連レッスン

要件

  1. ファイル先頭で import java.util.ArrayList; を書くこと
  2. ArrayList<Integer>new ArrayList<>() で生成すること (raw 型は禁止)
  3. 引数 a b c の順で末尾に add し、最後に toString() の結果を return すること

入出力例

test-cases.txt

buildList(10, 20, 30)"[10, 20, 30]" buildList(3, 1, 2)"[3, 1, 2]" buildList(5, 5, 5)"[5, 5, 5]"

ヒント

main.java
main.java
学習モード

メモ

ArrayList を使う

⌘S で保存