Javaプログラミングにおいて、データの集合を扱う「コレクション」は避けて通れない非常に重要な要素です。

その中でも「List(リスト)」は、要素の順序を保持し、重複を許容するという特性から、実務で最も頻繁に利用されるインターフェースの一つです。

本記事では、JavaにおけるListの基本的な使い方から、最新のJava 21などで導入された便利な機能、そしてパフォーマンスを意識した使い分けまで、テクニカルライターの視点で詳しく解説します。

JavaのList(リスト)とは何か

JavaのListは、java.utilパッケージに含まれるインターフェースであり、順序を持つ要素の集まりを管理するために使用されます。

配列(Array)と似ていますが、配列が作成時にサイズを固定しなければならないのに対し、Listは要素の追加や削除に合わせて動的にサイズが変更されるという大きなメリットがあります。

また、Listは「Generics(ジェネリクス)」を利用することで、格納する要素の型を制限し、型安全なプログラミングを可能にします。

例えば、List<String>と宣言すれば、そのリストには文字列しか入れることができず、コンパイル時に型チェックが行われるため、実行時のエラーを未然に防ぐことができます。

代表的なListの実装クラス

List自体はインターフェースであるため、実際に使用する際にはその実装クラスをインスタンス化する必要があります。

主に以下の2つがよく使われます。

ArrayList

内部的に配列を使用して要素を管理します。

インデックスによる要素へのアクセスが非常に高速ですが、リストの途中での挿入や削除には時間がかかる場合があります。

LinkedList

各要素が前後の要素への参照を持つ「双方向連結リスト」の構造を持ちます。

要素の挿入や削除が高速ですが、特定のインデックスへのアクセスには先頭から順に辿る必要があるため、低速です。

実務レベルでは、特別な理由がない限りArrayListを選択するのが一般的です。

これは、現代のコンピュータアーキテクチャにおいてメモリの連続性がキャッシュ効率に大きく寄与するため、ArrayListの方が総合的なパフォーマンスで勝ることが多いためです。

Listの初期化方法

Javaでは、用途に応じて様々なListの初期化方法が提供されています。

Javaのバージョンアップに伴い、より簡潔な記述が可能になっています。

インスタンス化による初期化

最も基本的な方法は、実装クラスを直接newすることです。

Java
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        // ArrayListのインスタンス化(Java 7以降はダイヤモンド演算子 <> で型推論可能)
        List<String> fruits = new ArrayList<>();
        
        fruits.add("Apple");
        fruits.add("Banana");
        
        System.out.println(fruits);
    }
}
実行結果
[Apple, Banana]

変更不可能なリストの作成(List.of)

Java 9以降、List.of()メソッドを使用することで、簡単に不変(Immutable)なリストを作成できるようになりました。

このメソッドで作成したリストに要素を追加したり削除したりしようとすると、UnsupportedOperationExceptionが発生します。

Java
import java.util.List;

public class Main {
    public static void main(String[] args) {
        // 要素を直接指定して不変リストを作成
        List<String> fixedList = List.of("Java", "Python", "C++");
        
        System.out.println(fixedList);
        
        // fixedList.add("Ruby"); // これは実行時にエラーになります
    }
}
実行結果
[Java, Python, C++]

既存の配列からの変換(Arrays.asList)

古くから使われている手法として、Arrays.asList()があります。

ただし、このメソッドで作成されたリストはサイズが固定される(要素の更新は可能だが、追加・削除は不可)という特殊な性質を持つため、注意が必要です。

Java
import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        String[] array = {"One", "Two", "Three"};
        List<String> list = Arrays.asList(array);
        
        // 内容の書き換えは可能
        list.set(0, "Zero");
        
        System.out.println(list);
    }
}
実行結果
[Zero, Two, Three]

基本的な操作(追加・取得・削除・確認)

Listを操作するための主要なメソッドを解説します。

これらの操作は、どの実装クラスでも共通して使用できます。

要素の追加(add)

add(element)で末尾に要素を追加し、add(index, element)で指定した位置に要素を挿入します。

Java
List<String> names = new ArrayList<>();
names.add("Tanaka"); // 末尾に追加
names.add("Sato");
names.add(1, "Suzuki"); // インデックス1の位置に挿入

要素の取得と更新(get / set)

特定のインデックスにある要素を取得するにはget(index)を、書き換えるにはset(index, element)を使用します。

Java
String name = names.get(0); // "Tanaka"を取得
names.set(2, "Watanabe"); // インデックス2の要素を書き換え

要素の削除(remove / clear)

要素の削除には、インデックスを指定する方法と、オブジェクトそのものを指定する方法があります。

Java
names.remove(0); // インデックス0の要素を削除
names.remove("Sato"); // "Sato"という値を持つ要素を削除
names.clear(); // 全ての要素を削除

リストの状態確認(size / isEmpty / contains)

リストの要素数を確認したり、特定の要素が含まれているかを判定したりするメソッドも多用されます。

Java
int count = names.size(); // 要素数を取得
boolean empty = names.isEmpty(); // 空かどうかを判定
boolean exists = names.contains("Tanaka"); // 指定した要素が含まれるか

Listの繰り返し処理(ループ)

リスト内の全要素に対して処理を行う方法はいくつかあります。

用途に応じて最適なものを選択しましょう。

拡張for文(Enhanced for-loop)

最も一般的で読みやすい方法です。

Java
List<String> list = List.of("A", "B", "C");
for (String item : list) {
    System.out.println(item);
}

forEachメソッド(Lambda式)

Java 8以降、関数型プログラミングのスタイルで記述できるようになりました。

Java
list.forEach(item -> System.out.println(item));
// メソッド参照を使うとさらに簡潔
list.forEach(System.out::println);

Iteratorによるループ

ループの途中で要素を削除する必要がある場合は、Iteratorを使用するのが安全です。

拡張for文の中でlist.remove()を呼び出すと、ConcurrentModificationExceptionが発生する原因となります。

Java
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5));
        Iterator<Integer> it = numbers.iterator();
        
        while (it.hasNext()) {
            if (it.next() % 2 == 0) {
                it.remove(); // 安全に削除可能
            }
        }
        System.out.println(numbers);
    }
}
実行結果
[1, 3, 5]

Stream APIによる応用操作

Java 8で導入されたStream APIを活用することで、リストのフィルタリングや変換、集計処理を非常に強力に記述できます

フィルタリングと変換

特定の条件に合うものだけを抽出し、新しいリストを作成する例です。

Java
import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<String> languages = List.of("Java", "JavaScript", "Python", "Ruby", "PHP");

        // "J"で始まる要素だけを抽出し、すべて大文字に変換してリスト化
        List<String> result = languages.stream()
                .filter(s -> s.startsWith("J"))
                .map(String::toUpperCase)
                .collect(Collectors.toList());

        System.out.println(result);
    }
}
実行結果
[JAVA, JAVASCRIPT]

リストのソート(並び替え)

sort()メソッドやStream APIを使用して、リストをソートできます。

Java
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Integer> nums = new ArrayList<>(List.of(5, 2, 8, 1, 9));

        // 昇順ソート
        nums.sort(Comparator.naturalOrder());
        System.out.println("昇順: " + nums);

        // 降順ソート
        nums.sort(Comparator.reverseOrder());
        System.out.println("降順: " + nums);
    }
}
実行結果
昇順: [1, 2, 5, 8, 9]
降順: [9, 8, 5, 2, 1]

Java 21の最新機能:Sequenced Collections

Java 21では、「Sequenced Collections」という新しいインターフェース体系が導入されました

これにより、Listの先頭や末尾へのアクセスがより直感的になりました。

これまで末尾の要素を取得するにはlist.get(list.size() - 1)と書く必要がありましたが、新しいメソッドによって簡潔に記述できます。

メソッド名説明
addFirst(e)先頭に要素を追加する
addLast(e)末尾に要素を追加する
getFirst()先頭の要素を取得する
getLast()末尾の要素を取得する
removeFirst()先頭の要素を削除する
removeLast()末尾の要素を削除する
reversed()逆順のビューを返す
Java
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        // Java 21のSequencedCollection機能を活用
        List<String> list = new ArrayList<>(List.of("Second", "Third"));
        
        list.addFirst("First"); // 先頭に追加
        list.addLast("Fourth"); // 末尾に追加
        
        System.out.println("First element: " + list.getFirst());
        System.out.println("Last element: " + list.getLast());
        System.out.println("Whole list: " + list);
        
        // 逆順のリストを生成
        List<String> reversedList = list.reversed();
        System.out.println("Reversed: " + reversedList);
    }
}
実行結果
First element: First
Last element: Fourth
Whole list: [First, Second, Third, Fourth]
Reversed: [Fourth, Third, Second, First]

List使用時のパフォーマンスと注意点

Listを効果的に使いこなすためには、計算量(アルゴリズムの効率)とメモリの振る舞いを理解しておくことが重要です。

ArrayList vs LinkedList の詳細比較

前述の通り、多くのケースではArrayListが推奨されます。

その理由を計算量の観点から表にまとめました。

操作ArrayListLinkedList
インデックスによるアクセスO(1) (非常に速い)O(n) (遅い)
末尾への追加O(1) (速い)O(1) (速い)
先頭・途中への挿入O(n) (要素のシフトが発生)O(1) (ポインタの付け替えのみ)
要素の検索O(n)O(n)

注意点:LinkedListは「挿入が速い」と言われますが、それは「挿入位置を特定できている場合」に限ります。

特定の位置(例えば真ん中)に挿入する場合、そこまで辿るコストがO(n)かかるため、実測値ではArrayListの方が速いケースも少なくありません。

初期容量の指定

ArrayListは内部の配列がいっぱいになると、新しい大きな配列を確保して全要素をコピーします。

追加する要素数が事前に分かっている場合は、コンストラクタで初期容量(Initial Capacity)を指定することで、この再確保のオーバーヘッドを削減できます

Java
// 10,000個の要素を入れることがわかっている場合
List<String> largeList = new ArrayList<>(10000);

スレッドセーフなList

標準のArrayListやLinkedListはスレッドセーフではありません。

マルチスレッド環境で複数のスレッドから同時に読み書きを行う場合は、以下のいずれかを検討する必要があります。

  • Collections.synchronizedList(new ArrayList<>()):全ての操作を同期化します。
  • CopyOnWriteArrayList:書き込み時に配列をコピーします。読み取り操作が圧倒的に多く、書き込みが稀なケースで非常に高いパフォーマンスを発揮します。

まとめ

JavaのListは、柔軟性と機能性を兼ね備えた非常に強力なデータ構造です。

  • 基本はArrayListを使用し、要素の動的な管理を行う
  • 不変なデータには List.of() を活用して安全性を高める
  • 大量のデータ操作には Stream API を使用して宣言的に記述する
  • Java 21以降は Sequenced Collections メソッド(getFirst/getLastなど)を利用してコードを簡潔にする

これらのポイントを抑えることで、読みやすくメンテナンス性の高いJavaプログラムを構築できるようになります。

ListはJavaコレクションフレームワークの土台となる部分ですので、まずは基本的なメソッドを使いこなし、徐々にStream APIや最新の機能を組み込んでいくのが上達の近道です。