結論から言うと、迷ったらまずArrayListを選べば、ほとんどの場面で困りません。
先頭での追加・削除や、Iteratorで位置を保ったままの挿入・削除が頻発するときだけLinkedListを検討してください。これがJavaのListの使い分けの基本線になります。
とはいえ、その根拠と、LinkedListを選ぶべき場面の見分け方がわからなければ、現場のコードレビューで指摘されたときに答えられません。
この記事では、その判断基準をArrayList・LinkedList・Vector・Stackの内部構造の違いから示し、add・get・removeなどの基本操作、Stream APIとの組み合わせ、不変Listの作り方まで、コード例つきで解説します。
駆け出しのころ、私はこの使い分けを知らずにパフォーマンスの悪いコードを書いた経験があります。同じ回り道をしないための一本です。
この記事は、Java入門ロードマップの「STEP2|基本文法」に対応しています。全体の学習順は次の記事で確認できます。
JavaのListとは
JavaのListは、複数のデータ(要素)を順序付けて格納するための入れ物です。
公式には「コレクションフレームワーク」という、データを扱うための仕組みの一部として提供されています。
Listの基本概念と特徴
Listインターフェースは、データを扱う上で便利な3つの大きな特徴を持っています。
ポイント
- 順序の保持: 追加した要素が、追加した順番通りに格納されます。0番目、1番目、2番目…というように、各要素にインデックスが割り振られるのが特徴です。
- 重複の許可: 同じ値を持つ要素を複数格納できます。例えば、"apple"という文字列を3つListに入れることも可能です。
- 動的なサイズ変更: 配列とは違い、格納する要素の数に応じてサイズを自動で変更してくれます。最初に「要素は10個まで」と決める必要はありません。
これらの特徴により、プログラムの実行中にデータ数が変動するような場面で、JavaのListは役立ちます。
Listのメリット・デメリット
便利なListにも、メリットとデメリットがあります。
メリット- 要素数の変更が容易: アプリケーションの実行中にデータが増減しても柔軟に対応可能です。
- 豊富なメソッド: 要素の追加、削除、検索、並び替えなど、データを操作するための便利なメソッドが多数用意されています。
- プリミティブ型を直接格納できない:
intやdoubleなどのプリミティブ型は直接格納できません。IntegerやDoubleといったラッパークラスへの変換が必要です。 - メモリ消費: 配列に比べて、付加的な情報(サイズなど)を保持するため、わずかに多くのメモリを消費します。
これらの特性を理解し、場面に応じて配列と使い分けるのが重要です。
Listの種類
List<E>インターフェース。new するのは下の実装クラスJavaのListはインターフェース(設計図)であり、実際に使う際にはそれを実装した具象クラスのインスタンスを生成します。
ArrayListとは
ArrayListは、JavaのListの中で最もよく使われるクラスの一つです。内部的には配列を利用してデータを管理しています。
特徴
- ランダムアクセスが高速: インデックスを指定して要素を取得する操作(
get()メソッド)が速いです。内部が配列なので、インデックスからメモリアドレスを直接計算できるためです。 - 要素の追加・削除は遅くなる: リストの末尾以外に要素を追加・削除すると、後続の要素をすべてずらす処理が発生するため、パフォーマンスが低下します。
日常的な開発では、まずArrayListを検討してください。データの参照が主な用途の場合に特に強みを発揮するListです。
LinkedListとは
LinkedListは、各要素が前後の要素への参照(リンク)を持つことで、リスト構造を実現しています。データが数珠つなぎになっているイメージです。
特徴
- 先頭への追加・削除や、Iteratorでの追加・削除が高速: 先頭・末尾への追加・削除や、
ListIteratorで今いる位置に対して行う追加・削除は、前後の要素のリンクを変更するだけで済むため、処理が高速です。ArrayListのように後続の要素をずらす必要がありません。ただし、インデックスを指定して中間に追加・削除する場合は、その位置まで要素をたどる時間がかかります。 - ランダムアクセスが遅い: インデックスを指定して要素を取得する場合、先頭または末尾のうち近いほうから順番に要素をたどっていく必要があります。そのため、リストの要素数が多いほど時間がかかります。
先頭への追加・削除や、Iteratorを使った追加・削除が頻繁に発生する処理には、LinkedListが適しています。
VectorとStackの違いと使い分け
VectorとStackは、古いバージョンのJavaから存在するクラスです。
VectorはArrayListとよく似ていますが、スレッドセーフであるという大きな違いがあります。
つまり、複数のスレッドから同時にアクセスしても、データが壊れないように設計されています。
ただし、その分パフォーマンスはArrayListに劣るため、2026年時点でもCollections.synchronizedList()や、java.util.concurrentパッケージのCopyOnWriteArrayListでスレッドセーフを実現します。
StackはVectorを継承したクラスで、LIFO(Last-In, First-Out)、つまり「後入れ先出し」のデータ構造を実現します。
本を積み上げて、上から取っていくイメージです。push()で要素を追加し、pop()で要素を取り出します。
2026年時点でも、Stackの代わりにArrayDequeを使うのがJava公式Javadocでも明記されているベストプラクティスです。
ArrayListとLinkedListの比較表で選ぶ
どのListを使うべきかの選び方
では、どのListを選べばよいのでしょうか。選択の基準はシンプルです。
- 要素の参照(取得)がメインで、追加・削除が少ない場合:
ArrayListを選びましょう。ほとんどのケースでこれが最適解です。 - リストの先頭への追加・削除や、Iteratorで位置を保ったままの追加・削除が頻繁に発生する場合:
LinkedListが適しています。 - マルチスレッド環境で、スレッドセーフなListが必要な場合:
ArrayListをCollections.synchronizedList()でラップするか、CopyOnWriteArrayListを検討します。 - スタック構造が必要な場合:
ArrayDequeを使用するのが現在のベストプラクティスです。
実務の感覚を付け加えると、最初からLinkedListを選ぶ判断をすることはほとんどありません。
要素数が数万件を超えるような規模でない限り、ArrayListの追加・削除コスト(後続要素のシフト)は体感できるほどの差になりにくく、むしろLinkedListが要素ごとに前後の参照を保持する分のメモリオーバーヘッドの方が問題になりやすいためです。
迷ったらArrayListから始め、プロファイリングで実際にボトルネックが見えてからLinkedListへの変更を検討する、という順序が安全です。
Listの使い方
ここからは、具体的なコードを見ながらJavaのListの使い方を学んでいきましょう。今回はArrayListを例に説明します。
Listの作成方法(ArrayList/LinkedListの初期化)
Listを生成する際は、Listインターフェースの変数に、ArrayListなどの具象クラスのインスタンスを代入します。
// String型の要素を格納するArrayListを作成
List<String> fruits = new ArrayList<>();
// Integer型の要素を格納するLinkedListを作成
List<Integer> numbers = new LinkedList<>();
このようにインターフェース型で宣言しておくと、後からArrayListをLinkedListに変更したくなった場合でも、修正箇所を最小限に抑えられます。
現場でよくある勘違いとして、Arrays.asList()で作ったListが挙げられます。
これは配列を裏側に持つ「固定長のビュー」を返すため、get()やset()は使えても、add()やremove()を呼ぶとUnsupportedOperationExceptionが発生します。
可変長のListが必要な場合は、new ArrayList<>(Arrays.asList(...))のようにコンストラクタでラップしてください。
Listの要素の追加・削除・取得
Listの基本的な操作は、以下のメソッドで行います。
- 追加:
add() - 取得:
get() - 更新:
set() - 削除:
remove() - 要素数取得:
size() - 空かどうかの判定:
isEmpty()
// Listの作成
List<String> languages = new ArrayList<>();
// 要素の追加 (add)
languages.add("Java");
languages.add("Python");
languages.add("Go");
System.out.println("追加後: " + languages); // 出力: [Java, Python, Go]
// 2番目の要素を取得 (get) ※インデックスは0から始まる
String lang = languages.get(1);
System.out.println("取得した要素: " + lang); // 出力: Python
// 2番目の要素を更新 (set)
languages.set(1, "Ruby");
System.out.println("更新後: " + languages); // 出力: [Java, Ruby, Go]
// 3番目の要素を削除 (remove)
languages.remove(2);
System.out.println("削除後: " + languages); // 出力: [Java, Ruby]
// 要素数を取得 (size)
int count = languages.size();
System.out.println("現在の要素数: " + count); // 出力: 2
Java 21以降では、ListがSequencedCollectionを実装するようになり、先頭・末尾要素へのアクセス専用メソッドが使えます。
get(0)やget(list.size() - 1)のようなインデックス計算をせずに済むため、意図が伝わりやすくなります。
List<String> team = new ArrayList<>(List.of("Java", "Kotlin", "Scala"));
// 先頭要素の取得(Java 21〜)
System.out.println(team.getFirst()); // 出力: Java
// 末尾要素の取得(Java 21〜)
System.out.println(team.getLast()); // 出力: Scala
イテレーションの方法(for文・for-each・Iterator)
Listに格納されたすべての要素を順番に処理するには、繰り返し処理(イテレーション)を使います。主に3つの方法があります。
拡張for文 (for-each)
最もシンプルで推奨される方法です。
List<String> animals = new ArrayList<>();
animals.add("dog");
animals.add("cat");
animals.add("rabbit");
for (String animal : animals) {
System.out.println(animal);
}
従来のfor文
インデックスを使って要素にアクセスします。
for (int i = 0; i < animals.size(); i++) {
System.out.println(animals.get(i));
}
Iterator
ループの最中に要素を安全に削除したい場合に使用します。
Iterator<String> iterator = animals.iterator();
while (iterator.hasNext()) {
String animal = iterator.next();
if (animal.equals("cat")) {
iterator.remove(); // 安全な削除方法
}
}
System.out.println(animals); // 出力: [dog, rabbit]
Listの並び替えと検索(Collectionsクラス活用)
Collectionsというユーティリティクラスを使うと、Listの並び替えや検索が簡単に行えます。
List<Integer> scores = new ArrayList<>();
scores.add(80);
scores.add(95);
scores.add(72);
// 昇順に並び替え (sort)
Collections.sort(scores);
System.out.println("昇順ソート後: " + scores); // 出力: [72, 80, 95]
// 降順に並び替え
Collections.sort(scores, Collections.reverseOrder());
System.out.println("降順ソート後: " + scores); // 出力: [95, 80, 72]
// バイナリサーチで要素を検索 (事前にソートが必要)
Collections.sort(scores); // まず昇順にソート
int index = Collections.binarySearch(scores, 80);
System.out.println("80のインデックス: " + index); // 出力: 1
Listの応用テクニック
基本的な使い方をマスターしたら、次はより高度なテクニックを見ていきましょう。
Listのコピーとサブリストの作成
Listのコピー
元のListとは別に、同じ要素を持つ新しいListを作成したい場合があります。
List<String> original = new ArrayList<>();
original.add("A");
original.add("B");
// コンストラクタを使ってコピー(シャローコピー)
List<String> copy = new ArrayList<>(original);
copy.add("C");
System.out.println("オリジナル: " + original); // 出力: [A, B]
System.out.println("コピー: " + copy); // 出力: [A, B, C]
サブリストの作成
subList()メソッドを使うと、Listの一部を切り出して新しいListとして扱えます。
List<Integer> numbers = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
numbers.add(i); // [1, 2, 3, 4, 5]
}
// インデックス1から3までを切り出す (3は含まない)
List<Integer> sub = numbers.subList(1, 3);
System.out.println("サブリスト: " + sub); // 出力: [2, 3]
// 注意: サブリストへの変更は元のリストにも反映される
sub.set(0, 99);
System.out.println("変更後のサブリスト: " + sub); // 出力: [99, 3]
System.out.println("変更後のオリジナル: " + numbers); // 出力: [1, 99, 3, 4, 5]
subList()は元のリストの「ビュー」を返すため、取り扱いには注意が必要です。
特に、サブリスト取得後に元のリストの構造そのものを変更(add/remove)すると、サブリスト側の操作でConcurrentModificationExceptionが発生します。
また、List.of()で作った不変Listに対してsubList()を呼んでも例外にはなりませんが、返るビューも不変のままです。
そのビューに対してadd()やset()を呼んだ時点でUnsupportedOperationExceptionになります。
ListとStream APIの活用
Java 8で導入されたStream APIを使うと、Listの集計やフィルタリング処理を簡潔に記述できます。
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 偶数だけをフィルタリングして、2倍にして、新しいListを作成する
List<Integer> result = numbers.stream() // ストリームを生成
.filter(n -> n % 2 == 0) // 偶数のみを抽出
.map(n -> n * 2) // 各要素を2倍にする
.collect(Collectors.toList()); // 結果をListに変換
System.out.println(result); // 出力: [4, 8, 12, 16, 20]
for文で書くよりも、何をしているかが一目でわかりやすいのが特徴です。
なお、Java 16以降ではcollect(Collectors.toList())の代わりにtoList()を直接呼べる短縮記法が使えます。
ただし挙動は同じではありません。
collect(Collectors.toList())が返すListは、現在の標準的なJDK実装では可変(要素の追加・削除が可能)ですが、これはAPI仕様として保証された挙動ではありません。
一方Stream.toList()が返すListは仕様上明確に不変で、add()を呼ぶとUnsupportedOperationExceptionになります。
不変List(Immutable List)の作り方
一度作成したら要素を変更・追加・削除されたくない、読み取り専用のListを作ります。これを「不変List」と呼びます。
Java 9以降: List.of()
// 要素を変更できないListを作成
List<String> immutableList = List.of("Java", "Python", "Go");
// immutableList.add("Ruby"); // UnsupportedOperationExceptionが発生する
Java 8以前: Collections.unmodifiableList()
List<String> modifiableList = new ArrayList<>();
modifiableList.add("Java");
modifiableList.add("Python");
// 変更不可能なビューを作成
List<String> unmodifiableList = Collections.unmodifiableList(modifiableList);
// unmodifiableList.add("Go"); // UnsupportedOperationExceptionが発生する
既存のListから不変Listを作る: List.copyOf()
List.of()が新規に要素を列挙して不変Listを作るのに対し、Java 10で追加されたList.copyOf()は既存のListをコピーして不変Listを作ります。
List<String> source = new ArrayList<>();
source.add("Java");
source.add("Kotlin");
// 既存のListをコピーして不変Listを作成
List<String> immutableCopy = List.copyOf(source);
// immutableCopy.add("Scala"); // UnsupportedOperationExceptionが発生する
不変Listは、データを安全に保ちたい場合に有効なテクニックです。
Listを使う際の注意点
スレッド安全性の問題
ArrayListやLinkedListは、スレッドセーフではありません。
複数のスレッドから同時に同じListインスタンスにアクセスして変更を加えると、予期せぬエラー(ConcurrentModificationExceptionなど)やデータの不整合が発生する可能性があります。
マルチスレッド環境でListを扱う場合は、以下のいずれかの方法を検討してください。
Collections.synchronizedList()でListをラップするjava.util.concurrentパッケージのCopyOnWriteArrayListクラスを使用する
パフォーマンスに影響する操作
Listの種類によって、得意な操作と苦手な操作があります。
ArrayList: リストの先頭や中間への要素の追加・削除は、後続の全要素をシフトさせるため、要素数が多いほどパフォーマンスが著しく低下します。LinkedList: インデックスを指定した要素の取得(get(index))は、先頭または末尾の近いほうから順にたどるため、要素数が多いほど時間がかかります。
ループの中でこれらの操作を多用すると、アプリケーション全体の性能に影響を与える可能性があるため、注意が必要です。
Null要素や重複の取り扱い
Listはnullや重複した要素を許容します。
これは柔軟である一方、意図しないnullが紛れ込むとNullPointerExceptionの原因になります。
また、重複を許したくないデータを扱う場合は、Setインターフェースを使うか、add()する前にcontains()メソッドで存在チェックを行うなどの工夫が必要です。
よくある質問
Q1. JavaのListとArrayListの違いは何ですか?
A1. Listはインターフェース(設計図)で、ArrayListはそれを実装した具象クラスです。実際にコードで使うときは、List型の変数にArrayListのインスタンスを代入します。ArrayListはListの実装の一つに過ぎず、他にLinkedListやVectorも同じListインターフェースを実装しています。
Q2. ArrayListとLinkedList、どちらを使えばいいですか?
A2. 迷ったらArrayListを選んでください。要素の取得が速く、多くの場面で最適です。先頭への要素の追加・削除や、Iteratorで位置を保ったままの追加・削除が頻繁に発生する処理のときだけ、LinkedListを検討します。
Q3. Listで重複した値は格納できますか?
A3. はい、格納できます。Listは同じ値を持つ要素を複数格納できるデータ構造です。重複を許したくない場合は、SetインターフェースやListのcontains()メソッドでの事前チェックを使います。
Q4. Listはスレッドセーフですか?
A4. いいえ、ArrayListとLinkedListはスレッドセーフではありません。マルチスレッド環境ではCollections.synchronizedList()でラップするか、CopyOnWriteArrayListを使います。
Q5. Listを変更できないようにするにはどうすればいいですか?
A5. Java 9以降ならList.of()で新規の不変Listを作成できます。既存のListから不変Listを作りたい場合は、Java 10で追加されたList.copyOf()を使います。どちらも要素の追加・削除を試みるとUnsupportedOperationExceptionが発生します。
Q6. Stream APIでListを作るtoList()とcollect(Collectors.toList())の違いは何ですか?
A6. Java 16以降のtoList()が返すListは不変で、要素の追加・削除ができません。一方、collect(Collectors.toList())が返すListは可変です。用途に応じて使い分けてください。
まとめ
この記事では、JavaのListの基本概念から種類、使い方、応用テクニック、注意点までを解説しました。
- Listは順序を保持し、重複を許す動的なデータ構造
-
普段使いは
ArrayList、先頭での追加・削除が多いならLinkedListを選ぶ -
基本的な操作は
add,get,remove,setで行う - Stream APIや不変Listなどの応用テクニックも活用しよう
JavaのListは、あらゆる開発で登場する基本中の基本です。しかし、その特性をきちんと理解しているかどうかで、書けるコードの品質は大きく変わります。
| REV. | DATE | 内容 |
|---|---|---|
| 5 | 2026-10-16 | 図解を描き直し(図面のデザインにそろえた) |
| 4 | 2026-10-14 | 一人称と言い回しの表記をそろえた。雰囲気だけの挿絵を外し、図解に置き換え |
| 3 | 2026-10-04 | 記載内容の事実確認と修正 |
| 2 | 2026-08-10 | 2026年8月時点の情報で全体を見直し(古い記述を6件更新) |
| 1 | 2025-10-30 | 初版 |







