はじめに (対象読者・この記事でわかること)

この記事は、Javaの基礎を学んだプログラミング初心者や、オブジェクト指向でのデータ操作に不安を抱える開発者を対象としています。
本稿を読むことで、Listインターフェースの役割、ArrayList と LinkedList の違い、そして実務で使えるリスト操作のベストプラクティスが理解でき、サンプルコードを通じて実際に手を動かしながら実装できるようになります。
執筆のきっかけは、日常の業務で「リストの性能差が原因で処理が遅くなる」場面に遭遇したことから、正しい選択と使い方を共有したいと考えたためです。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。
- Java の基本的な文法(クラス、メソッド、例外処理など)
- IDE(IntelliJ IDEA, Eclipse 等)の基本操作

Listの概要と選択基準

Java のコレクションフレームワークは、データ構造を統一的に扱えるよう設計されており、その中心に位置するのが List インターフェース です。List は「順序付けられた要素の集合」を表し、重複を許容します。主な実装クラスとして ArrayListLinkedList があり、内部構造と性能が大きく異なります。

  • ArrayList は内部的に配列を使用し、インデックスによる高速なランダムアクセスが可能です。一方、要素の追加・削除は末尾以外では配列の再確保や要素シフトが必要になるため、コストが高くなります。
  • LinkedList は双方向連結リストで構成され、先頭・末尾への追加・削除が O(1) で行えますが、インデックスによるアクセスは O(n) になるため、検索が頻繁なケースでは不向きです。

この章では、どのようなシナリオでどちらを選択すべきかを具体例とともに解説します。

1. 小規模・検索中心のケース

データ件数が数百件程度で、頻繁に要素を検索したりインデックスで取得したりする場合は ArrayList が適しています。内部配列のキャッシュ効率が高く、CPU キャッシュに乗りやすいため、実行速度が速くなります。

2. 大規模・頻繁な挿入・削除のケース

データ件数が数千件以上で、リストの中間で要素を頻繁に挿入・削除する必要がある場合は LinkedList が有利です。ノードのリンクだけを書き換えれば済むため、再配置コストが抑えられます。

3. スレッドセーフが必要な場合

List 自体は非同期環境で安全ではありません。マルチスレッドで共有する場合は Collections.synchronizedListCopyOnWriteArrayList など、スレッドセーフなラッパーを利用します。

Listの実装と活用:具体的なコード例

以下では、ArrayList と LinkedList の基本的な使い方から、実務で役立つ高度なテクニックまで段階的に紹介します。サンプルはすべて Java 17 で動作することを前提にしています。

ステップ1:List の生成と基本操作

Java
import java.util.*; public class ListDemo { public static void main(String[] args) { // ArrayList の生成 List<String> arrayList = new ArrayList<>(); arrayList.add("Apple"); arrayList.add("Banana"); arrayList.add("Cherry"); // LinkedList の生成 List<String> linkedList = new LinkedList<>(); linkedList.add("Dog"); linkedList.add("Elephant"); linkedList.add("Frog"); // 要素取得と探索 System.out.println("ArrayList の 2 番目: " + arrayList.get(1)); System.out.println("LinkedList に Frog があるか: " + linkedList.contains("Frog")); // イテレーション for (String fruit : arrayList) { System.out.println("Fruit: " + fruit); } } }

ポイント: - インターフェース型 List で変数を宣言することで、実装の差し替えが容易になる。 - add, get, contains などの基本メソッドはどちらの実装でも同様に使用できる。

ステップ2:中間挿入と削除のベンチマーク

実務では「リストの途中に大量データを挿入」するケースが多く、実装選択が性能に直結します。以下は JMH(Java Microbenchmark Harness)を用いた簡易ベンチマーク例です。

Java
import org.openjdk.jmh.annotations.*; import java.util.*; import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Thread) public class ListInsertBenchmark { private List<Integer> arrayList; private List<Integer> linkedList; private static final int SIZE = 10000; private static final int INSERT_POS = 5000; @Setup(Level.Invocation) public void setUp() { arrayList = new ArrayList<>(SIZE); linkedList = new LinkedList<>(); for (int i = 0; i < SIZE; i++) { arrayList.add(i); linkedList.add(i); } } @Benchmark public void arrayListInsert() { arrayList.add(INSERT_POS, -1); } @Benchmark public void linkedListInsert() { linkedList.add(INSERT_POS, -1); } }

結果例(JDK 17, 2GHz CPU): - arrayListInsert:約 150 µs
- linkedListInsert:約 2.3 µs

この差は、ArrayList が内部配列のシフトを行う必要があるのに対し、LinkedList はノード参照の更新だけで済むためです。大量の中間挿入が頻発する場合は LinkedList が有利です。

ステップ3:ストリーム API と組み合わせた高度な操作

Java 8 以降、Stream API を使用して List を関数型スタイルで処理できます。以下は、リストの要素をフィルタリングし、ソートして別リストへ格納する例です。

Java
List<String> names = List.of("Alice", "Bob", "Charlie", "David", "Eve"); // "A" で始まる名前だけを抽出し、長さ順にソート List<String> result = names.stream() .filter(s -> s.startsWith("A")) .sorted(Comparator.comparingInt(String::length)) .toList(); // Java 16 以降 System.out.println(result); // [Alice]

ポイント: - List.of は不変リストを生成し、toList() で再び不変リストが返るため、安全なデータフローが実現できる。 - 変更が必要な場合は collect(Collectors.toCollection(ArrayList::new)) のように可変リストへ変換すれば OK。

ハマった点やエラー解決

1. ConcurrentModificationException が出た

原因: for-each でリストを走査中に list.remove() した。
解決策: Iterator を取得し、iterator.remove() を使用するか、removeIf メソッドで一括削除する。

Java
Iterator<String> it = list.iterator(); while (it.hasNext()) { if (it.next().startsWith("A")) { it.remove(); // 安全に削除 } }

2. IndexOutOfBoundsException が頻発した

原因: list.add(index, element)index が現在のサイズを超えていた。
解決策: size() を事前に確認し、add 前に ensureCapacity(ArrayList)や addFirst/addLast(LinkedList)を利用する。

パフォーマンス最適化のヒント

ケース 推奨 List 主な理由
ランダムアクセスが頻繁 ArrayList O(1) のインデックス取得
先頭・末尾の頻繁な挿入/削除 LinkedList O(1) のノード操作
スレッドセーフが必要 CopyOnWriteArrayList 読み取りが多数で書き込みが少ない場合に有効
不変リストで安全に共有 List.of 変更不可でメモリ効率が高い

まとめ

本記事では、Java の List インターフェースと代表的な実装(ArrayList, LinkedList)の特徴、選択基準、実装例、そしてパフォーマンスチューニングについて解説しました。

  • List の基本概念と実装の違いを理解し、用途に合わせた選択ができるようになった
  • 実際のコードで ArrayList と LinkedList の使い分けを体験し、ベンチマーク結果から性能差を把握できた
  • Stream API と組み合わせたモダンなリスト操作や、ConcurrentModificationException の回避策を身につけた

これらの知識は、日常的な業務ロジックだけでなく、パフォーマンスが要求される大規模データ処理にも応用可能です。次回は、Java 21 のレコードと List の組み合わせや、Reactive Streams における List の扱いについて掘り下げる予定です。

参考資料