List インターフェースの概要
List<E> インターフェースは、順序付けられた要素のコレクションを定義します。リスト内の各要素には特定のインデックス(位置)があり、そのインデックスを使って要素にアクセスできます。Listは重複要素を許可するため、同じ要素を複数回追加することが可能です。
List の主な特徴
- 順序付け: 要素は挿入された順序で保持されます
- インデックスベース: 0から始まるインデックスで要素にアクセス可能
- 重複許容: 同じ要素を複数含むことができます
- Null許容: null要素を格納できます(実装による)
主な List 実装クラス
ArrayList
ArrayList は動的配列に基づく List インターフェースの実装です。内部的には要素を配列に格納し、容量が不足すると自動的に拡張されます。
ArrayList の特徴
- ランダムアクセスが高速: get(i)、set(i, e) 操作は O(1) の時間計算量
- 末尾への追加は通常高速: add(e) 操作はほとんどの場合 O(1)、ただし容量拡張が必要な場合は O(n)
- 中間位置への挿入・削除は低速: add(i, e)、remove(i) は O(n) の時間計算量
- イテレーションは効率的: 連続したメモリ領域を使用するため、キャッシュフレンドリー
- スレッドセーフではない: 同期化されていないため、複数スレッドでの使用には外部同期が必要
ArrayList の基本的な使用例
import java.util.ArrayList;
import java.util.List;
public class ArrayListExample {
public static void main(String[] args) {
// ArrayList の作成
List<String> fruits = new ArrayList<>();
// 要素の追加
fruits.add("りんご");
fruits.add("バナナ");
fruits.add("オレンジ");
// 特定の位置に要素を挿入
fruits.add(1, "ぶどう"); // ["りんご", "ぶどう", "バナナ", "オレンジ"]
// 要素へのアクセス
String fruit = fruits.get(0); // "りんご"
// 要素の変更
fruits.set(0, "青りんご"); // ["青りんご", "ぶどう", "バナナ", "オレンジ"]
// 要素の削除
fruits.remove(2); // "バナナ"を削除 -> ["青りんご", "ぶどう", "オレンジ"]
// リストのサイズ取得
int size = fruits.size(); // 3
// 要素の検索
int index = fruits.indexOf("ぶどう"); // 1
boolean contains = fruits.contains("バナナ"); // false
// リストの走査
for (String f : fruits) {
System.out.println(f);
}
}
}
LinkedList
LinkedList は双方向リンクリストに基づく List インターフェースの実装です。各要素(ノード)は前後の要素への参照を持ちます。
LinkedList の特徴
- 要素の挿入・削除が高速: 位置が分かっている場合、add(i, e)、remove(i) は O(1)
- 要素へのアクセスは低速: get(i)、set(i, e) は O(n) の時間計算量(先頭・末尾を除く)
- List と Queue/Deque の両方として使用可能: List, Queue, Deque インターフェースを全て実装
- メモリ使用量が多い: 各要素が前後への参照を保持するため、ArrayList より多くのメモリを使用
- イテレーションは相対的に遅い: メモリの局所性が低いため、キャッシュミスが発生しやすい
LinkedList の基本的な使用例
import java.util.LinkedList;
import java.util.List;
public class LinkedListExample {
public static void main(String[] args) {
// LinkedList の作成
LinkedList<String> names = new LinkedList<>();
// 要素の追加(List として使用)
names.add("田中");
names.add("佐藤");
// 先頭・末尾への追加(Deque として使用)
names.addFirst("山田"); // ["山田", "田中", "佐藤"]
names.addLast("鈴木"); // ["山田", "田中", "佐藤", "鈴木"]
// 先頭・末尾の要素を取得
String first = names.getFirst(); // "山田"
String last = names.getLast(); // "鈴木"
// 先頭・末尾の要素を削除
names.removeFirst(); // "山田"を削除 -> ["田中", "佐藤", "鈴木"]
names.removeLast(); // "鈴木"を削除 -> ["田中", "佐藤"]
// キューとして使用
names.offer("高橋"); // ["田中", "佐藤", "高橋"]
String head = names.poll(); // "田中"を取得して削除 -> ["佐藤", "高橋"]
// スタックとして使用
names.push("伊藤"); // ["伊藤", "佐藤", "高橋"]
String top = names.pop(); // "伊藤"を取得して削除 -> ["佐藤", "高橋"]
}
}
ArrayList と LinkedList の使い分け
操作 | ArrayList | LinkedList | 推奨される実装 |
---|---|---|---|
ランダムアクセス(get/set) | O(1) - 高速 | O(n) - 低速 | ArrayList |
先頭への追加/削除 | O(n) - 低速 | O(1) - 高速 | LinkedList |
末尾への追加 | 償却 O(1) - 高速 | O(1) - 高速 | ArrayList |
中間への挿入/削除 | O(n) - 低速 | O(n) - 低速(ただし実際の操作は高速) | LinkedList(位置が分かっている場合) |
イテレーション | 効率的 | 比較的非効率的 | ArrayList |
メモリ使用量 | 少ない | 多い | ArrayList |
選択のガイドライン
- ArrayListを選ぶ場合:
- ランダムアクセス(get/set)が多い
- リストサイズが比較的安定している
- 主に末尾への追加操作が多い
- メモリ効率が重要
- LinkedListを選ぶ場合:
- 頻繁な挿入・削除操作が多い(特に先頭/末尾)
- スタックやキューとしても使用したい
- リスト全体のイテレーションよりも要素の操作が主
Vector と Stack - レガシーな List 実装
Vector と Stack は Java 1.0 から存在する古い実装で、同期化されています(thread-safe)。一般的に、これらのレガシークラスの代わりに、ArrayList や LinkedList と Collections.synchronizedList() の組み合わせ、または java.util.concurrent パッケージのクラスを使用することが推奨されています。
同期化されたリストの作成
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SynchronizedListExample {
public static void main(String[] args) {
// 非同期リスト
List<Integer> numbers = new ArrayList<>();
// 同期化されたリスト
List<Integer> synchronizedList = Collections.synchronizedList(numbers);
// 同期化されたリストを使用する場合、イテレーションには外部同期が必要
synchronized (synchronizedList) {
for (Integer number : synchronizedList) {
// 安全にアクセス
System.out.println(number);
}
}
}
}
効率的なリスト操作のためのテクニック
1. 初期容量の指定
ArrayList を使用する際、あらかじめ必要な要素数が分かっている場合は、初期容量を指定することで再割り当てのオーバーヘッドを減らすことができます。
// 10,000要素を格納することが分かっている場合
List<String> largeList = new ArrayList<>(10000);
2. サブリストの活用
リストの一部だけを操作したい場合は、subList() メソッドを使用してビューを取得できます。これはオリジナルのリストを参照するため、メモリ効率が良いです。
List<String> fullList = new ArrayList<>();
// fullListに要素を追加...
// インデックス10から20までのサブリストを取得
List<String> subList = fullList.subList(10, 20);
// サブリストを操作するとオリジナルリストも変更される
subList.clear(); // fullListのインデックス10〜19の要素が削除される
3. リスト間の操作
複数のリスト間での操作には、containsAll()、addAll()、removeAll()、retainAll() などの便利なメソッドが用意されています。
List<String> list1 = new ArrayList<>();
list1.add("A"); list1.add("B"); list1.add("C");
List<String> list2 = new ArrayList<>();
list2.add("B"); list2.add("C"); list2.add("D");
// 和集合: list1とlist2の全要素(重複なし)
List<String> union = new ArrayList<>(list1);
union.addAll(list2); // [A, B, C, B, C, D]
// 積集合: list1とlist2の共通要素
List<String> intersection = new ArrayList<>(list1);
intersection.retainAll(list2); // [B, C]
// 差集合: list1にあってlist2にない要素
List<String> difference = new ArrayList<>(list1);
difference.removeAll(list2); // [A]
4. リストのソート
リストは Collections.sort() メソッドまたは List.sort() メソッド(Java 8以降)を使用してソートできます。
List<Integer> numbers = new ArrayList<>();
numbers.add(5); numbers.add(2); numbers.add(8); numbers.add(1);
// 自然順序でのソート
Collections.sort(numbers); // [1, 2, 5, 8]
// または
numbers.sort(null); // Java 8以降
// カスタム比較器を使用したソート
numbers.sort((a, b) -> b - a); // 降順 [8, 5, 2, 1]
Java 8以降の Stream API との連携
Java 8以降では、List は Stream API と組み合わせて強力なデータ処理が可能になります。
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class ListStreamExample {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("山田太郎"); names.add("鈴木一郎");
names.add("佐藤花子"); names.add("田中次郎");
// フィルタリング: "田"を含む名前だけを取得
List<String> filteredNames = names.stream()
.filter(name -> name.contains("田"))
.collect(Collectors.toList());
// [山田太郎, 田中次郎]
// マッピング: 名前の長さのリストを作成
List<Integer> nameLengths = names.stream()
.map(String::length)
.collect(Collectors.toList());
// [4, 4, 4, 4]
// ソート: 名前を長さでソート
List<String> sortedNames = names.stream()
.sorted((a, b) -> a.length() - b.length())
.collect(Collectors.toList());
// [山田太郎, 鈴木一郎, 佐藤花子, 田中次郎]
}
}
まとめ
List インターフェースは Java コレクションフレームワークの中で最も広く使用されるインターフェースの一つです。適切な実装(ArrayList または LinkedList)を選択し、効率的な操作方法を理解することで、パフォーマンスを最適化できます。
- ArrayList: ランダムアクセスが多い場合や、リスト全体の走査が頻繁な場合に適しています。
- LinkedList: 頻繁な挿入・削除操作が必要な場合や、スタックやキューとしても使用したい場合に適しています。
多くの場合、ArrayList がデフォルトの選択肢として適しています。特定のユースケースで LinkedList が明確な利点を提供する場合にのみ、LinkedList の使用を検討してください。