信頼度ランク
| S | 公式ソース確認済み |
| A | 成功実績多数・失敗例少数 |
| B | 賛否両論 |
| C | 動作未確認・セキュリティリスク高 |
| Z | 個人所感 |
Java ArrayListの全て ─ 内部構造・動的リサイズ・設計思想・試験の急所まで完全理解
JavaのArrayListをゼロから完全解説。Collections Framework誕生の背景、内部の配列リサイズ戦略、ジェネリクス・オートボクシング、主要メソッド、Silver試験頻出パターンまで網羅。
一言結論
ArrayList は『内部に配列を持ち、足りなくなったら 1.5 倍に拡張する可変長リスト』。Java 1.2 の Collections Framework で導入され、ジェネリクス(Java 5)で型安全になった。配列の固定長という制約を克服するために生まれた、Java で最も使われるデータ構造である。
Java ArrayListの全て ─ 内部構造・動的リサイズ・設計思想・試験の急所まで完全理解
この記事を読めば: ArrayList がなぜ存在するのか、中身はどうなっているのか、配列との違いは何か、メソッドの挙動はどうなるか、Silver試験でどこが狙われるかまで、一本の線でつながる。
なぜ ArrayList が生まれたか ─ 配列の限界と Collections Framework
配列の3つの限界
配列は高速で原始的なデータ構造だが、実務では3つの問題がある。
- サイズが固定: 要素数が事前に分からないとき、大きめに作るか毎回作り直すしかない
- 挿入・削除が面倒: 途中に入れるには後ろの要素を全部ずらす処理を自分で書く必要がある
- 便利メソッドがない: ソート・検索・削除などを毎回自分で実装するか
Arraysクラスを使う
// 配列で「要素を追加」しようとすると……
int[] arr = {1, 2, 3};
// arr に 4 を追加したい → サイズ変更できないので新配列を作るしかない
int[] newArr = Arrays.copyOf(arr, arr.length + 1);
newArr[3] = 4;
// これを毎回書くのは辛い
Collections Framework の誕生(Java 1.2, 1998年)
Java 1.0(1996年)には Vector と Hashtable という2つのコレクションクラスがあった。しかしこの2つには問題があった。
- 全メソッドが
synchronized: マルチスレッド対応のためにロックを取るが、シングルスレッドでも常にロックのオーバーヘッドが発生する - 設計に統一性がない:
VectorとHashtableに共通のインターフェースがなく、API がバラバラ
Java の設計者 Joshua Bloch は、Java 1.2 で Collections Framework を一から設計した。その中心にあるのが以下のインターフェース階層だ。
Iterable
└── Collection
├── List ← 順序あり・重複あり・インデックスアクセス
├── Set ← 重複なし
└── Queue ← 先入れ先出し
ArrayList は List インターフェースの最も基本的な実装として、このフレームワークの中核に位置する。
Vector → ArrayList ─ 何が変わったか
Vector(Java 1.0) | ArrayList(Java 1.2) | |
|---|---|---|
| 同期(synchronized) | 全メソッドに付いている | なし(高速) |
| 拡張倍率 | 2倍 | 1.5倍(メモリ効率が良い) |
| インターフェース | 独自 | List を実装 |
| 現在の立ち位置 | 非推奨(レガシークラス) | 標準 |
同期が必要な場面では Collections.synchronizedList(new ArrayList<>()) や CopyOnWriteArrayList を使う。Vector を使う理由は2026年の今、一切ない。
ArrayList の内部構造 ─ 中身は「ただの配列」
内部実装を覗く
ArrayList の正体は驚くほどシンプルだ。OpenJDKのソースコードを見ると、核心は2つのフィールドだけ。
// ArrayList の内部(概念的に簡略化)
public class ArrayList<E> {
Object[] elementData; // 要素を格納する配列
int size; // 実際の要素数(elementData.length ≧ size)
}
ArrayList は内部に Object[] という普通の配列を持っている。add() で要素を追加すると size が増え、配列が足りなくなったら大きな配列を新たに作ってコピーする。
add("A") → add("B") → add("C") の過程:
elementData: [A][_][_][_][_][_][_][_][_][_] (容量10, size=1)
elementData: [A][B][_][_][_][_][_][_][_][_] (容量10, size=2)
elementData: [A][B][C][_][_][_][_][_][_][_] (容量10, size=3)
※ [_] は未使用スロット
動的リサイズの仕組み ─ 1.5倍成長
デフォルトの初期容量(capacity)は 10。要素数が容量に達すると、現在の容量の 1.5倍の新しい配列を作り、全要素をコピーする。
// OpenJDK の grow メソッド(概念的に簡略化)
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍(ビットシフトで計算)
// 新しい配列を作って古い配列からコピー
elementData = Arrays.copyOf(elementData, newCapacity);
return elementData;
}
1.5倍にする理由: 2倍(Vector の方式)だとメモリの無駄が大きくなる。1.5倍なら平均して約25%の空きスロットで済む。一方で拡張回数は2倍方式より多くなるが、コピーの総コストは償却計算量 O(1) で変わらない。メモリ効率とパフォーマンスのバランスが取れた選択だ。
capacity と size の違い
ArrayList<String> list = new ArrayList<>(); // capacity=10, size=0
list.add("A"); // capacity=10, size=1
list.add("B"); // capacity=10, size=2
System.out.println(list.size()); // 2(実際の要素数)
// capacity は直接取得するメソッドがない(内部実装の詳細)
┌─────────────────────────────────┐
│ [A] [B] [_] [_] [_] [_] [_] [_] [_] [_] │
│ ← size=2 → │
│ ← ─────── capacity=10 ─────────────── → │
└─────────────────────────────────┘
size(): 実際に格納されている要素数- capacity: 内部配列の長さ(リサイズなしで格納できる上限)
初期容量の指定 ─ パフォーマンスチューニング
要素数がだいたい分かっているなら、初期容量を指定することでリサイズ回数を減らせる。
// 10000件のデータを格納すると分かっている場合
ArrayList<String> list = new ArrayList<>(10000);
// リサイズが一度も発生しない → コピーのオーバーヘッドなし
逆に大量の要素を削除した後、メモリを解放したい場合は trimToSize() を使う。
list.trimToSize(); // capacity を size に縮小(余分なメモリを解放)
ジェネリクス ─ 型安全な ArrayList
Java 5 以前(2004年以前)の世界
Java 1.2〜1.4 の ArrayList にはジェネリクスがなかった。何でも入れられたが、取り出すときにキャストが必要だった。
// Java 1.4 以前のスタイル(危険)
ArrayList list = new ArrayList();
list.add("hello");
list.add(42); // String と Integer が混在(コンパイルは通る)
String s = (String) list.get(0); // OK
String t = (String) list.get(1); // ❌ ClassCastException! (Integer を String にキャスト)
Java 5 のジェネリクスで解決
// Java 5+ のスタイル(安全)
ArrayList<String> list = new ArrayList<String>();
list.add("hello");
// list.add(42); // ❌ コンパイルエラー(コンパイル時に型ミスを検出)
String s = list.get(0); // キャスト不要
ダイヤモンド演算子(Java 7)
// Java 5-6: 右辺にも型を書く必要があった
ArrayList<String> list = new ArrayList<String>();
// Java 7+: 右辺は <> で省略できる(型推論)
ArrayList<String> list = new ArrayList<>();
var(Java 10)
// Java 10+: ローカル変数の型推論
var list = new ArrayList<String>(); // list は ArrayList<String> 型と推論される
型消去(Type Erasure)─ ジェネリクスの裏側
ジェネリクスの型情報はコンパイル時にチェックされた後、消される。実行時の ArrayList<String> と ArrayList<Integer> は同じ ArrayList クラスだ。
ArrayList<String> a = new ArrayList<>();
ArrayList<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); // true(同じクラス)
この「型消去」は Java 5 がジェネリクスを導入したときに、既存のバイトコードとの後方互換性を保つために選択された設計だ。代償として、実行時に型パラメータを取得できない。
プリミティブ型とオートボクシング
プリミティブ型は ArrayList に直接入れられない
// ❌ コンパイルエラー
// ArrayList<int> list = new ArrayList<>();
// ✅ ラッパー型を使う
ArrayList<Integer> list = new ArrayList<>();
なぜか? ジェネリクスは型消去によって内部的に Object として扱う。プリミティブ型は Object を継承しないため、ジェネリクスのパラメータに使えない。
オートボクシング / アンボクシング
Java 5 から、プリミティブ型とラッパー型の変換は自動で行われる。
ArrayList<Integer> list = new ArrayList<>();
list.add(42); // int 42 → Integer.valueOf(42)(オートボクシング)
int n = list.get(0); // Integer 42 → int 42(オートアンボクシング)
null のアンボクシングは NullPointerException
ArrayList<Integer> list = new ArrayList<>();
list.add(null); // null を格納(許可される)
int n = list.get(0); // ❌ NullPointerException!(null を int にアンボクシングできない)
試験の罠:
Integerのリストにnullを入れて、それをintに代入するコードは頻出。
ボクシングのパフォーマンスコスト
オートボクシングは便利だが、大量のデータを扱うときはオーバーヘッドになる。
// ❌ 非効率(100万回のボクシングが発生)
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
list.add(i); // 毎回 Integer オブジェクトが生成される
}
// ✅ プリミティブ配列を使う(大量のデータにはこちらが適切)
int[] arr = new int[1_000_000];
for (int i = 0; i < arr.length; i++) {
arr[i] = i; // オブジェクト生成なし
}
これが「配列が今も必要な理由」の一つだ。ArrayList<Integer> は各要素が Integer オブジェクト(16バイト〜)なのに対し、int[] は各要素が4バイトで済む。
主要メソッド ─ 全部コードで理解する
追加
ArrayList<String> list = new ArrayList<>();
list.add("A"); // 末尾に追加 → [A]
list.add("B"); // 末尾に追加 → [A, B]
list.add("C"); // 末尾に追加 → [A, B, C]
list.add(1, "X"); // インデックス1に挿入(後ろをずらす)→ [A, X, B, C]
ArrayList<String> other = new ArrayList<>(List.of("Y", "Z"));
list.addAll(other); // 別のコレクションを全部追加 → [A, X, B, C, Y, Z]
list.addAll(2, other); // インデックス2に挿入 → [A, X, Y, Z, B, C, Y, Z]
add(index, element) は内部で System.arraycopy を使って後ろの要素をずらす。リストが大きいと遅い(O(n))。末尾への add() は償却 O(1)。
取得
ArrayList<String> list = new ArrayList<>(List.of("A", "B", "C", "D"));
String first = list.get(0); // "A"
String last = list.get(list.size() - 1); // "D"
int size = list.size(); // 4
boolean empty = list.isEmpty(); // false
// ❌ 範囲外アクセス
// list.get(4); // IndexOutOfBoundsException
// list.get(-1); // IndexOutOfBoundsException
get(i) は内部配列への直接アクセスなので O(1)。これが ArrayList 最大の強みだ。
更新
ArrayList<String> list = new ArrayList<>(List.of("A", "B", "C"));
String old = list.set(1, "X"); // インデックス1を "X" に置換。戻り値は古い値 "B"
System.out.println(list); // [A, X, C]
set() は戻り値として置換前の値を返す。試験で問われるポイント。
検索
ArrayList<String> list = new ArrayList<>(List.of("A", "B", "C", "B", "D"));
int first = list.indexOf("B"); // 1(最初に見つかった位置)
int last = list.lastIndexOf("B"); // 3(最後に見つかった位置)
int none = list.indexOf("Z"); // -1(見つからない場合)
boolean has = list.contains("C"); // true
indexOf / contains は先頭から順に equals() で比較する。O(n)。
削除 ─ 最大の罠
ここが Silver 試験で最も問われるポイントだ。
ArrayList<String> list = new ArrayList<>(List.of("A", "B", "C", "D"));
// インデックス指定で削除
String removed = list.remove(1); // インデックス1の "B" を削除。戻り値は "B"
System.out.println(list); // [A, C, D]
// 値指定で削除
boolean found = list.remove("C"); // 最初に見つかった "C" を削除。戻り値は true/false
System.out.println(list); // [A, D]
ここまでは分かりやすい。問題は List<Integer> の場合。
ArrayList<Integer> nums = new ArrayList<>(List.of(10, 20, 30, 20));
// remove(int index): インデックス指定
nums.remove(1); // インデックス1の要素(20)を削除 → [10, 30, 20]
// remove(Object o): 値指定 ── Integer にキャストが必要!
nums.remove(Integer.valueOf(30)); // 値30を削除 → [10, 20]
// 罠: remove(20) は何を意味するか?
// nums.remove(20); // ❌ int 20 → インデックス20 → IndexOutOfBoundsException!
List<Integer> で値を削除したいときは Integer.valueOf(値) を使う。これを忘れると remove(int) のオーバーロードが優先され、インデックス削除になる。
試験での出題パターン:
remove(1)が「値1の削除」か「インデックス1の削除」かを問う。Java のオーバーロード解決では、ボクシングより完全一致が優先されるため、remove(1)はremove(int)に解決される。
一括操作
ArrayList<String> list = new ArrayList<>(List.of("A", "B", "C", "A", "B"));
// 条件に合う要素を一括削除(Java 8+)
list.removeIf(s -> s.equals("A"));
System.out.println(list); // [B, C, B]
// 全要素を変換(Java 8+)
list.replaceAll(String::toLowerCase);
System.out.println(list); // [b, c, b]
// 全消去
list.clear();
System.out.println(list.isEmpty()); // true
ソート
ArrayList<Integer> nums = new ArrayList<>(List.of(5, 3, 1, 4, 2));
// 自然順序(昇順)
nums.sort(Comparator.naturalOrder());
System.out.println(nums); // [1, 2, 3, 4, 5]
// 降順
nums.sort(Comparator.reverseOrder());
System.out.println(nums); // [5, 4, 3, 2, 1]
// null 許容ソート
ArrayList<String> mixed = new ArrayList<>(Arrays.asList("B", null, "A"));
mixed.sort(Comparator.nullsFirst(Comparator.naturalOrder()));
System.out.println(mixed); // [null, A, B]
部分リスト
ArrayList<String> list = new ArrayList<>(List.of("A", "B", "C", "D", "E"));
List<String> sub = list.subList(1, 4); // [B, C, D](1含む〜4含まない)
// sub は元リストの「ビュー」── 元リストと連動する
sub.set(0, "X");
System.out.println(list); // [A, X, C, D, E](元リストも変わる!)
sub.clear();
System.out.println(list); // [A, E](範囲削除のショートカットとして使える)
イテレーション ─ ループの書き方と罠
4つのループ方法
ArrayList<String> list = new ArrayList<>(List.of("A", "B", "C"));
// 1. 従来の for ループ(インデックスが必要なとき)
for (int i = 0; i < list.size(); i++) {
System.out.println(i + ": " + list.get(i));
}
// 2. for-each(最も読みやすい)
for (String s : list) {
System.out.println(s);
}
// 3. Iterator(ループ中に削除が必要なとき)
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.equals("B")) it.remove();
}
// 4. forEach + ラムダ(Java 8+)
list.forEach(s -> System.out.println(s));
list.forEach(System.out::println); // メソッド参照
ConcurrentModificationException ─ ループ中に直接削除すると爆発する
ArrayList<String> list = new ArrayList<>(List.of("A", "B", "C", "B", "D"));
// ❌ for-each 中に list.remove() → ConcurrentModificationException
// for (String s : list) {
// if (s.equals("B")) list.remove(s);
// }
// ✅ Iterator の remove() を使う
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().equals("B")) {
it.remove(); // Iterator 経由なら安全
}
}
System.out.println(list); // [A, C, D]
// ✅ removeIf を使う(最も簡潔)
list.removeIf(s -> s.equals("C"));
System.out.println(list); // [A, D]
なぜ ConcurrentModificationException が起きるか? ArrayList は内部に modCount というカウンタを持っている。add や remove が呼ばれるたびにインクリメントされる。for-each は内部で Iterator を使うが、Iterator は生成時の modCount を記憶している。ループ中に list.remove() で modCount が変わると、Iterator が「自分以外の誰かがリストを変更した」と判断して例外を投げる。
ArrayList と配列の使い分け
| 観点 | 配列 (int[], String[]) | ArrayList |
|---|---|---|
| サイズ | 固定 | 可変 |
| プリミティブ格納 | 直接格納(高速・省メモリ) | ラッパー型経由(オーバーヘッドあり) |
| ランダムアクセス | O(1) | O(1)(内部は配列) |
| 末尾への追加 | 不可(新配列を作るしかない) | O(1) 償却 |
| 途中への挿入・削除 | 手動でずらす | add(i, e) / remove(i) |
| 型安全 | コンパイル時 + 実行時(共変性の罠あり) | コンパイル時(ジェネリクス) |
| メモリ効率 | 最良(ヘッダ + 要素のみ) | やや多い(容量 > 要素数の場合の空き) |
| 便利メソッド | Arrays クラス | List インターフェースの全メソッド |
使い分けの指針:
- 要素数が決まっていて変わらない → 配列
- プリミティブ型の大量データ(数値計算、画像処理) → 配列
- それ以外 →
ArrayList
実務では ArrayList を使う場面が圧倒的に多い。
Arrays.asList() vs List.of() vs new ArrayList<>()
この3つの違いは Silver 試験の超頻出ポイント。
// 1. Arrays.asList() ── 固定サイズ List(元の配列と連動)
String[] arr = {"A", "B", "C"};
List<String> fixed = Arrays.asList(arr);
fixed.set(0, "Z"); // ✅(要素の更新は可能。arr[0] も "Z" に変わる)
// fixed.add("D"); // ❌ UnsupportedOperationException
// fixed.remove(0); // ❌ UnsupportedOperationException
// 2. List.of() ── 完全不変 List(Java 9+)
List<String> immutable = List.of("A", "B", "C");
// immutable.set(0, "Z"); // ❌ UnsupportedOperationException
// immutable.add("D"); // ❌ UnsupportedOperationException
// List.of(null); // ❌ NullPointerException(null 不可!)
// 3. new ArrayList<>() ── 完全に可変な List
List<String> mutable = new ArrayList<>(List.of("A", "B", "C"));
mutable.set(0, "Z"); // ✅
mutable.add("D"); // ✅
mutable.remove(0); // ✅
mutable.add(null); // ✅(null 許容)
Arrays.asList | List.of | new ArrayList<> | |
|---|---|---|---|
set(更新) | ✅ | ❌ | ✅ |
add / remove | ❌ | ❌ | ✅ |
null 格納 | ✅ | ❌ | ✅ |
| 元の配列と連動 | する | しない | しない |
| 用途 | 配列のラッパー | 定数リスト | 汎用 |
試験の罠:
Arrays.asList()で作ったリストにadd()を呼ぶコード、List.of()にnullを入れるコードが頻出。
ArrayList vs LinkedList
List インターフェースには ArrayList 以外に LinkedList という実装がある。
内部構造の違い
ArrayList の内部:
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ C │ D │ _ │ _ │ ← 連続した配列
└───┴───┴───┴───┴───┴───┘
LinkedList の内部:
[prev|A|next] ↔ [prev|B|next] ↔ [prev|C|next] ↔ [prev|D|next]
← 各ノードがポインタで繋がっている(双方向連結リスト)
パフォーマンス比較
| 操作 | ArrayList | LinkedList |
|---|---|---|
get(i) ランダムアクセス | O(1) | O(n)(先頭から辿る) |
末尾 add | O(1) 償却 | O(1) |
先頭 add(0, e) | O(n)(全要素をずらす) | O(1) |
途中 add(i, e) | O(n) | O(n)(位置の探索 + 挿入自体は O(1)) |
| メモリ | 少ない | 多い(ノードごとにポインタ2本分) |
| キャッシュ効率 | 高い(連続メモリ → CPU キャッシュに乗りやすい) | 低い(ノードが散らばる) |
結論: 迷ったら ArrayList
LinkedList が ArrayList に勝つケースは実務ではほとんどない。「先頭への頻繁な挿入・削除」でも、最新のCPUではキャッシュ効率の差で ArrayList の方が速いことが多い。Java のコアライブラリ開発者ですら「LinkedList を使うべき場面は思いつかない」と発言している。
List インターフェースへのプログラミング
なぜ ArrayList ではなく List で受けるのか
// ❌ 具象クラスで宣言(実装に依存)
ArrayList<String> list = new ArrayList<>();
// ✅ インターフェースで宣言(実装に非依存)
List<String> list = new ArrayList<>();
List で宣言しておけば、後で LinkedList や CopyOnWriteArrayList に差し替えても呼び出し側のコードを変える必要がない。これはインターフェースへのプログラミングと呼ばれる設計原則で、Collections Framework 全体の設計思想だ。
// メソッドのパラメータも List で受ける
public void process(List<String> items) {
// ArrayList でも LinkedList でも動く
for (String item : items) {
System.out.println(item);
}
}
Silver試験の急所 ─ ArrayList で問われるパターン
パターン1: remove のオーバーロード
List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4, 5));
list.remove(2); // インデックス2 → 値3を削除 → [1, 2, 4, 5]
list.remove(Integer.valueOf(2)); // 値2を削除 → [1, 4, 5]
System.out.println(list); // [1, 4, 5]
パターン2: Arrays.asList の変更可能範囲
List<String> list = Arrays.asList("A", "B", "C");
list.set(1, "X"); // ✅ [A, X, C]
list.add("D"); // ❌ UnsupportedOperationException
パターン3: List.of と null
List<String> list = List.of("A", null, "C"); // ❌ NullPointerException
パターン4: ConcurrentModificationException
List<String> list = new ArrayList<>(List.of("A", "B", "C"));
for (String s : list) {
if (s.equals("B")) list.remove(s); // ❌ ConcurrentModificationException
}
パターン5: set の戻り値
List<String> list = new ArrayList<>(List.of("A", "B", "C"));
String old = list.set(1, "X");
System.out.println(old); // "B"(置換前の値が返る)
System.out.println(list); // [A, X, C]
パターン6: subList はビュー
List<String> list = new ArrayList<>(List.of("A", "B", "C", "D"));
List<String> sub = list.subList(1, 3); // [B, C](ビュー)
sub.clear();
System.out.println(list); // [A, D](元リストからも消える)
パターン7: size() vs length vs length()
ArrayList<String> list = new ArrayList<>();
String s = "hello";
String[] arr = {"a", "b"};
System.out.println(list.size()); // 0 ← コレクションは size()
System.out.println(s.length()); // 5 ← String は length()
System.out.println(arr.length); // 2 ← 配列は length(フィールド)
まとめ
| 特性 | ArrayList |
|---|---|
| 内部構造 | Object[] 配列 + size カウンタ |
| 初期容量 | 10(コンストラクタで指定可) |
| 拡張倍率 | 1.5倍 |
| ランダムアクセス | O(1) |
| 末尾 add | O(1) 償却 |
| 途中 add / remove | O(n) |
| プリミティブ格納 | 不可(ラッパー型経由) |
| null 格納 | 可能 |
| スレッド安全 | 非安全(必要なら synchronizedList や CopyOnWriteArrayList) |
| 歴史 | Java 1.2(1998年)で Vector の後継として導入 |
ArrayList は Java で最も使われるデータ構造だ。内部は「ただの配列 + 自動リサイズ」というシンプルな仕組みで、配列の固定長という制約を解消している。配列を理解した上で ArrayList を学ぶと、「なぜ O(1) なのか」「なぜ途中への挿入が遅いのか」「なぜ Vector が非推奨になったのか」がすべて腑に落ちる。
参考
- java.util.ArrayList (Java SE 21)
- java.util.List (Java SE 21)
- JLS §15.12 - Method Invocation Expressions(オーバーロード解決の仕様)
- Joshua Bloch, Effective Java, 3rd Edition — Item 28: Prefer lists to arrays