はじめに#
Javaで複数のデータを効率的に管理するには、コレクションフレームワークの理解が不可欠です。配列は固定長で機能が限られていますが、コレクションを使えば可変長のデータを柔軟に追加・削除・検索できます。
この記事では、Collectionインターフェースの階層構造から、ArrayList・LinkedList・HashSet・TreeSet・HashMap・TreeMapといった主要な実装クラスの特徴と使い分け、そしてJava 21で導入されたSequenced Collectionsまで、段階的に解説します。
コレクションフレームワークの全体像#
コレクションフレームワークは、データの集合を扱うための統一されたアーキテクチャです。配列と比較して以下のメリットがあります。
| 特徴 |
配列 |
コレクション |
| サイズ |
固定長 |
可変長(自動拡張) |
| 格納できる型 |
プリミティブ型・参照型 |
参照型のみ |
| 提供される機能 |
length、添字アクセスのみ |
追加・削除・検索・ソートなど多数 |
| 重複の制御 |
不可 |
Setで重複排除可能 |
インターフェースの階層構造#
コレクションフレームワークは、用途に応じた複数のインターフェースで構成されています。
graph TD
subgraph "Collectionインターフェース系統"
Iterable["Iterable<E>"]
Collection["Collection<E>"]
List["List<E>"]
Set["Set<E>"]
Queue["Queue<E>"]
SortedSet["SortedSet<E>"]
NavigableSet["NavigableSet<E>"]
Deque["Deque<E>"]
Iterable --> Collection
Collection --> List
Collection --> Set
Collection --> Queue
Set --> SortedSet
SortedSet --> NavigableSet
Queue --> Deque
end
subgraph "Mapインターフェース系統"
Map["Map<K,V>"]
SortedMap["SortedMap<K,V>"]
NavigableMap["NavigableMap<K,V>"]
Map --> SortedMap
SortedMap --> NavigableMap
end主要なインターフェースの役割#
| インターフェース |
役割 |
特徴 |
Collection |
コレクションの基本操作 |
add、remove、contains等 |
List |
順序付きのコレクション |
インデックスによるアクセス、重複を許可 |
Set |
重複を許さないコレクション |
数学的な集合の概念 |
Queue |
先入れ先出し(FIFO)のコレクション |
キュー操作(offer、poll、peek) |
Deque |
両端キュー |
両端からの追加・削除が可能 |
Map |
キーと値のペアを管理 |
キーによる高速検索 |
Listインターフェース#
Listは要素の順序を保持し、重複を許可するコレクションです。インデックス(0始まり)を使って要素にアクセスできます。
ArrayListの特徴と使い方#
ArrayListは内部で可変長配列を使用した実装です。ランダムアクセスが高速で、最も一般的に使用されるListの実装クラスです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
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("りんご"); // 重複OK
System.out.println(fruits); // [りんご, バナナ, オレンジ, りんご]
// インデックスによるアクセス(O(1))
String first = fruits.get(0);
System.out.println("最初の要素: " + first); // りんご
// 要素の更新
fruits.set(1, "ぶどう");
System.out.println(fruits); // [りんご, ぶどう, オレンジ, りんご]
// 要素の削除(インデックス指定)
fruits.remove(2);
System.out.println(fruits); // [りんご, ぶどう, りんご]
// 要素数の取得
System.out.println("要素数: " + fruits.size()); // 3
}
}
|
ArrayListのパフォーマンス特性#
| 操作 |
計算量 |
説明 |
| get(index) |
O(1) |
インデックスによる直接アクセス |
| add(element) |
O(1) 償却 |
末尾への追加(容量拡張時はO(n)) |
| add(index, element) |
O(n) |
途中への挿入(要素のシフトが必要) |
| remove(index) |
O(n) |
削除後に要素のシフトが必要 |
| contains(element) |
O(n) |
線形検索 |
初期容量の指定#
大量のデータを格納することがわかっている場合は、初期容量を指定することでパフォーマンスを向上できます。
1
2
3
4
5
6
7
|
// 初期容量を1000に設定
List<Integer> numbers = new ArrayList<>(1000);
// 容量拡張による再配置を避けられる
for (int i = 0; i < 1000; i++) {
numbers.add(i);
}
|
LinkedListの特徴と使い方#
LinkedListは双方向連結リストで実装されています。先頭・末尾への追加・削除が高速ですが、ランダムアクセスは低速です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import java.util.LinkedList;
import java.util.List;
public class LinkedListExample {
public static void main(String[] args) {
LinkedList<String> tasks = new LinkedList<>();
// 先頭・末尾への追加(O(1))
tasks.addFirst("タスクA");
tasks.addLast("タスクB");
tasks.add("タスクC"); // 末尾に追加
System.out.println(tasks); // [タスクA, タスクB, タスクC]
// 先頭・末尾の取得
System.out.println("先頭: " + tasks.getFirst()); // タスクA
System.out.println("末尾: " + tasks.getLast()); // タスクC
// 先頭・末尾からの削除(O(1))
String removed = tasks.removeFirst();
System.out.println("削除: " + removed); // タスクA
System.out.println(tasks); // [タスクB, タスクC]
}
}
|
LinkedListのパフォーマンス特性#
| 操作 |
計算量 |
説明 |
| get(index) |
O(n) |
先頭または末尾から順に辿る |
| addFirst/addLast |
O(1) |
先頭・末尾への追加 |
| add(index, element) |
O(n) |
挿入位置まで辿る必要がある |
| removeFirst/removeLast |
O(1) |
先頭・末尾からの削除 |
ArrayListとLinkedListの使い分け#
graph TD
A["データの操作パターンは?"] --> B{"ランダムアクセスが多い"}
A --> C{"先頭・末尾の追加削除が多い"}
A --> D{"途中への挿入削除が多い"}
B --> E["ArrayList を選択"]
C --> F["LinkedList を選択"]
D --> G["要素数が少なければ ArrayList<br/>多ければ LinkedList を検討"]
style E fill:#90EE90
style F fill:#87CEEB
style G fill:#FFE4B5実務ではほとんどの場合ArrayListが適しています。LinkedListはメモリオーバーヘッドが大きく、CPUキャッシュ効率も悪いため、先頭・末尾の操作が頻繁な場合のみ検討してください。
Setインターフェース#
Setは重複を許さないコレクションです。数学的な「集合」の概念を表現しており、同じ要素は1つしか存在できません。
HashSetの特徴と使い方#
HashSetはハッシュテーブルで実装されており、要素の追加・削除・検索が非常に高速です。ただし、順序は保証されません。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
import java.util.HashSet;
import java.util.Set;
public class HashSetExample {
public static void main(String[] args) {
Set<String> colors = new HashSet<>();
// 要素の追加
colors.add("赤");
colors.add("青");
colors.add("緑");
colors.add("赤"); // 重複は無視される
System.out.println(colors); // [赤, 緑, 青](順序は不定)
System.out.println("要素数: " + colors.size()); // 3
// 要素の存在確認(O(1))
System.out.println("赤は含まれる: " + colors.contains("赤")); // true
System.out.println("黄は含まれる: " + colors.contains("黄")); // false
// 要素の削除
colors.remove("青");
System.out.println(colors); // [赤, 緑]
}
}
|
HashSetのパフォーマンス特性#
| 操作 |
計算量 |
説明 |
| add(element) |
O(1) |
ハッシュ値による直接配置 |
| remove(element) |
O(1) |
ハッシュ値による直接アクセス |
| contains(element) |
O(1) |
ハッシュ値による直接検索 |
カスタムオブジェクトとequals/hashCode#
HashSetでカスタムオブジェクトを使用する場合、equals()とhashCode()を適切にオーバーライドする必要があります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class CustomObjectHashSet {
public static void main(String[] args) {
Set<User> users = new HashSet<>();
users.add(new User(1, "田中"));
users.add(new User(2, "鈴木"));
users.add(new User(1, "田中")); // 重複(追加されない)
System.out.println("ユーザー数: " + users.size()); // 2
}
}
class User {
private int id;
private String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return id == user.id && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
}
|
LinkedHashSetで挿入順序を保持#
LinkedHashSetは内部でハッシュテーブルと双方向連結リストを組み合わせており、挿入順序を保持します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
import java.util.LinkedHashSet;
import java.util.Set;
public class LinkedHashSetExample {
public static void main(String[] args) {
Set<String> orderedColors = new LinkedHashSet<>();
orderedColors.add("赤");
orderedColors.add("青");
orderedColors.add("緑");
// 挿入順序が保持される
System.out.println(orderedColors); // [赤, 青, 緑]
}
}
|
TreeSetの特徴と使い方#
TreeSetは赤黒木(Red-Black Tree) で実装されており、要素が自然順序(昇順) でソートされます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
import java.util.Set;
import java.util.TreeSet;
public class TreeSetExample {
public static void main(String[] args) {
Set<Integer> numbers = new TreeSet<>();
numbers.add(30);
numbers.add(10);
numbers.add(50);
numbers.add(20);
numbers.add(40);
// 自動的にソートされる
System.out.println(numbers); // [10, 20, 30, 40, 50]
}
}
|
TreeSetの追加機能(NavigableSet)#
TreeSetはNavigableSetインターフェースを実装しており、範囲検索などの高度な操作が可能です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
import java.util.NavigableSet;
import java.util.TreeSet;
public class NavigableSetExample {
public static void main(String[] args) {
NavigableSet<Integer> numbers = new TreeSet<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.add(40);
numbers.add(50);
// 指定値以下で最大の要素
System.out.println("floor(25): " + numbers.floor(25)); // 20
// 指定値以上で最小の要素
System.out.println("ceiling(25): " + numbers.ceiling(25)); // 30
// 部分集合の取得
System.out.println("subSet(20, 40): " + numbers.subSet(20, 40)); // [20, 30]
// 降順ビュー
System.out.println("降順: " + numbers.descendingSet()); // [50, 40, 30, 20, 10]
}
}
|
TreeSetのパフォーマンス特性#
| 操作 |
計算量 |
説明 |
| add(element) |
O(log n) |
ツリーへの挿入 |
| remove(element) |
O(log n) |
ツリーからの削除 |
| contains(element) |
O(log n) |
ツリーの探索 |
| first() / last() |
O(log n) |
最小/最大要素の取得 |
カスタムComparatorでソート順を指定#
デフォルトの自然順序とは異なるソート順を指定する場合、Comparatorを使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;
public class CustomComparatorTreeSet {
public static void main(String[] args) {
// 降順でソート
Set<String> descendingNames = new TreeSet<>(Comparator.reverseOrder());
descendingNames.add("佐藤");
descendingNames.add("田中");
descendingNames.add("鈴木");
System.out.println(descendingNames); // [鈴木, 田中, 佐藤]
// 文字列長でソート
Set<String> byLength = new TreeSet<>(Comparator.comparingInt(String::length));
byLength.add("りんご");
byLength.add("ぶどう");
byLength.add("いちご");
// 長さが同じ場合は最初の要素のみ保持される点に注意
System.out.println(byLength); // [りんご]
}
}
|
Mapインターフェース#
Mapはキーと値のペアを管理するデータ構造です。キーは重複できませんが、値は重複できます。
HashMapの特徴と使い方#
HashMapはハッシュテーブルで実装されており、キーによる値の取得が非常に高速です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
import java.util.HashMap;
import java.util.Map;
public class HashMapExample {
public static void main(String[] args) {
Map<String, Integer> scores = new HashMap<>();
// エントリの追加
scores.put("田中", 85);
scores.put("鈴木", 90);
scores.put("佐藤", 78);
// 値の取得(O(1))
System.out.println("田中の点数: " + scores.get("田中")); // 85
// 存在しないキー
System.out.println("山田の点数: " + scores.get("山田")); // null
// デフォルト値付きの取得
int yamadaScore = scores.getOrDefault("山田", 0);
System.out.println("山田の点数(デフォルト): " + yamadaScore); // 0
// キーの存在確認
System.out.println("鈴木は存在: " + scores.containsKey("鈴木")); // true
// 値の更新
scores.put("田中", 95); // 上書き
System.out.println("田中の新しい点数: " + scores.get("田中")); // 95
// エントリの削除
scores.remove("佐藤");
System.out.println(scores); // {田中=95, 鈴木=90}
}
}
|
HashMapの便利なメソッド#
Java 8以降、HashMapには便利なメソッドが多数追加されています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
import java.util.HashMap;
import java.util.Map;
public class HashMapAdvanced {
public static void main(String[] args) {
Map<String, Integer> wordCount = new HashMap<>();
String[] words = {"apple", "banana", "apple", "orange", "banana", "apple"};
// 従来の書き方
for (String word : words) {
if (wordCount.containsKey(word)) {
wordCount.put(word, wordCount.get(word) + 1);
} else {
wordCount.put(word, 1);
}
}
wordCount.clear();
// merge を使った簡潔な書き方
for (String word : words) {
wordCount.merge(word, 1, Integer::sum);
}
System.out.println(wordCount); // {orange=1, banana=2, apple=3}
// computeIfAbsent: キーが存在しない場合のみ計算して追加
Map<String, StringBuilder> logs = new HashMap<>();
logs.computeIfAbsent("info", k -> new StringBuilder()).append("初期化完了 ");
logs.computeIfAbsent("info", k -> new StringBuilder()).append("処理開始 ");
System.out.println(logs.get("info")); // 初期化完了 処理開始
}
}
|
LinkedHashMapで挿入順序を保持#
LinkedHashMapは挿入順序を保持するHashMapです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import java.util.LinkedHashMap;
import java.util.Map;
public class LinkedHashMapExample {
public static void main(String[] args) {
Map<String, String> capitals = new LinkedHashMap<>();
capitals.put("日本", "東京");
capitals.put("アメリカ", "ワシントンD.C.");
capitals.put("イギリス", "ロンドン");
// 挿入順序が保持される
for (Map.Entry<String, String> entry : capitals.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
// 日本 -> 東京
// アメリカ -> ワシントンD.C.
// イギリス -> ロンドン
}
}
|
TreeMapの特徴と使い方#
TreeMapはキーを自然順序(昇順) でソートして保持します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import java.util.Map;
import java.util.TreeMap;
public class TreeMapExample {
public static void main(String[] args) {
Map<Integer, String> rankingMap = new TreeMap<>();
rankingMap.put(3, "銅メダル");
rankingMap.put(1, "金メダル");
rankingMap.put(2, "銀メダル");
// キーでソートされる
for (Map.Entry<Integer, String> entry : rankingMap.entrySet()) {
System.out.println(entry.getKey() + "位: " + entry.getValue());
}
// 1位: 金メダル
// 2位: 銀メダル
// 3位: 銅メダル
}
}
|
TreeMapの追加機能(NavigableMap)#
TreeMapはNavigableMapインターフェースを実装しており、範囲検索が可能です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import java.util.NavigableMap;
import java.util.TreeMap;
public class NavigableMapExample {
public static void main(String[] args) {
NavigableMap<Integer, String> studentGrades = new TreeMap<>();
studentGrades.put(60, "可");
studentGrades.put(70, "良");
studentGrades.put(80, "優");
studentGrades.put(90, "秀");
int score = 75;
// score以下で最大のキーに対応する値
var entry = studentGrades.floorEntry(score);
System.out.println(score + "点の評価: " + entry.getValue()); // 良
// 部分マップの取得(70点以上80点未満)
System.out.println("部分マップ: " + studentGrades.subMap(70, 80)); // {70=良}
}
}
|
Mapのパフォーマンス比較#
| 実装クラス |
get |
put |
順序 |
用途 |
| HashMap |
O(1) |
O(1) |
なし |
高速な検索が必要な場合 |
| LinkedHashMap |
O(1) |
O(1) |
挿入順 |
順序を保持したい場合 |
| TreeMap |
O(log n) |
O(log n) |
キー順 |
ソートや範囲検索が必要な場合 |
コレクションの操作#
コレクションを効率的に操作するための基本的なパターンを紹介します。
要素の追加と削除#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class CollectionOperations {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// 単一要素の追加
list.add("A");
list.add("B");
// 複数要素の追加
list.addAll(Arrays.asList("C", "D", "E"));
System.out.println(list); // [A, B, C, D, E]
// 条件に合う要素を削除(Java 8以降)
list.removeIf(s -> s.equals("C"));
System.out.println(list); // [A, B, D, E]
// すべての要素を削除
list.clear();
System.out.println("要素数: " + list.size()); // 0
}
}
|
ループ処理のパターン#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class IterationPatterns {
public static void main(String[] args) {
List<String> fruits = List.of("りんご", "バナナ", "オレンジ");
// 拡張for文(推奨)
for (String fruit : fruits) {
System.out.println(fruit);
}
// forEach(ラムダ式)
fruits.forEach(fruit -> System.out.println(fruit));
// forEachとメソッド参照
fruits.forEach(System.out::println);
// インデックスが必要な場合
for (int i = 0; i < fruits.size(); i++) {
System.out.println(i + ": " + fruits.get(i));
}
// Mapのループ
Map<String, Integer> scores = new HashMap<>();
scores.put("田中", 85);
scores.put("鈴木", 90);
// entrySetでループ(推奨)
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// forEachでループ
scores.forEach((name, score) ->
System.out.println(name + ": " + score));
}
}
|
ループ中の要素削除#
ループ中に要素を削除する場合は、Iteratorを使用するか、removeIfを使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class RemoveDuringIteration {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
// 方法1: Iteratorを使用
Iterator<Integer> iterator = numbers.iterator();
while (iterator.hasNext()) {
if (iterator.next() % 2 == 0) {
iterator.remove();
}
}
System.out.println(numbers); // [1, 3, 5]
// 方法2: removeIfを使用(推奨)
List<Integer> numbers2 = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
numbers2.removeIf(n -> n % 2 == 0);
System.out.println(numbers2); // [1, 3, 5]
}
}
|
不変コレクションの作成#
Java 9以降、List.of()、Set.of()、Map.of()で不変コレクションを簡単に作成できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
import java.util.List;
import java.util.Map;
import java.util.Set;
public class ImmutableCollections {
public static void main(String[] args) {
// 不変List
List<String> immutableList = List.of("A", "B", "C");
// 不変Set
Set<Integer> immutableSet = Set.of(1, 2, 3);
// 不変Map
Map<String, Integer> immutableMap = Map.of(
"one", 1,
"two", 2,
"three", 3
);
// 変更しようとすると UnsupportedOperationException
try {
immutableList.add("D");
} catch (UnsupportedOperationException e) {
System.out.println("不変コレクションは変更できません");
}
}
}
|
Sequenced Collections(Java 21の新機能)#
Java 21では、Sequenced Collectionsという新しいインターフェース群が導入されました。これにより、順序を持つコレクションに対して統一的なAPIで先頭・末尾の操作や逆順処理が可能になりました。
従来の問題点#
Java 21以前は、コレクションの最初・最後の要素を取得する方法がバラバラでした。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// Java 20以前の書き方(統一性がない)
List<String> list = new ArrayList<>();
String first = list.get(0); // List
String last = list.get(list.size() - 1); // List(煩雑)
Deque<String> deque = new ArrayDeque<>();
String first2 = deque.getFirst(); // Deque
String last2 = deque.getLast(); // Deque
SortedSet<String> sortedSet = new TreeSet<>();
String first3 = sortedSet.first(); // SortedSet
String last3 = sortedSet.last(); // SortedSet
LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
String first4 = linkedHashSet.iterator().next(); // 先頭のみ可能
// 末尾の取得は困難...
|
SequencedCollectionインターフェース#
Java 21では、SequencedCollectionインターフェースが追加され、順序を持つすべてのコレクションで統一的なAPIを使用できるようになりました。
graph TD
subgraph "Java 21 Sequenced Collections"
SC["SequencedCollection<E>"]
SS["SequencedSet<E>"]
SM["SequencedMap<K,V>"]
Collection --> SC
SC --> List
SC --> Deque
SC --> SS
SS --> SortedSet
SS --> LinkedHashSet
Map --> SM
SM --> SortedMap
SM --> LinkedHashMap
endSequencedCollectionの主要メソッド#
| メソッド |
説明 |
getFirst() |
最初の要素を取得 |
getLast() |
最後の要素を取得 |
addFirst(E) |
先頭に要素を追加 |
addLast(E) |
末尾に要素を追加 |
removeFirst() |
先頭の要素を削除して返す |
removeLast() |
末尾の要素を削除して返す |
reversed() |
逆順のビューを返す |
SequencedCollectionの使用例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.SequencedCollection;
public class SequencedCollectionExample {
public static void main(String[] args) {
// ArrayList(Listの実装)
var list = new ArrayList<String>();
list.add("A");
list.add("B");
list.add("C");
// 統一されたAPIで先頭・末尾にアクセス
System.out.println("List先頭: " + list.getFirst()); // A
System.out.println("List末尾: " + list.getLast()); // C
// LinkedHashSetでも同じAPI
var linkedSet = new LinkedHashSet<String>();
linkedSet.add("X");
linkedSet.add("Y");
linkedSet.add("Z");
System.out.println("Set先頭: " + linkedSet.getFirst()); // X
System.out.println("Set末尾: " + linkedSet.getLast()); // Z
// 先頭・末尾への追加
list.addFirst("先頭");
list.addLast("末尾");
System.out.println(list); // [先頭, A, B, C, 末尾]
}
}
|
reversed()メソッドによる逆順処理#
reversed()メソッドは、元のコレクションの逆順ビューを返します。ビューなので、新しいコレクションは作成されません。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
import java.util.ArrayList;
import java.util.List;
public class ReversedExample {
public static void main(String[] args) {
var numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5));
// 逆順ビューの取得
var reversed = numbers.reversed();
// 逆順でループ
System.out.println("逆順:");
for (int n : reversed) {
System.out.print(n + " "); // 5 4 3 2 1
}
System.out.println();
// Streamでも逆順処理が簡単に
numbers.reversed().stream()
.forEach(n -> System.out.print(n + " ")); // 5 4 3 2 1
System.out.println();
// 逆順ビューへの変更は元のコレクションに反映される
reversed.addFirst(100); // 逆順の先頭 = 元の末尾
System.out.println("元のリスト: " + numbers); // [1, 2, 3, 4, 5, 100]
}
}
|
SequencedMapの使用例#
SequencedMapはMapに対する順序付き操作を提供します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
import java.util.LinkedHashMap;
import java.util.SequencedMap;
public class SequencedMapExample {
public static void main(String[] args) {
SequencedMap<String, Integer> map = new LinkedHashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
// 最初と最後のエントリ
System.out.println("最初のエントリ: " + map.firstEntry()); // A=1
System.out.println("最後のエントリ: " + map.lastEntry()); // C=3
// 先頭・末尾への追加
map.putFirst("Z", 0); // 先頭に追加
map.putLast("D", 4); // 末尾に追加
System.out.println("マップ: " + map); // {Z=0, A=1, B=2, C=3, D=4}
// 逆順ビュー
System.out.println("逆順: " + map.reversed()); // {D=4, C=3, B=2, A=1, Z=0}
// sequencedKeySet(), sequencedValues(), sequencedEntrySet()
System.out.println("キー(順序付き): " + map.sequencedKeySet());
}
}
|
コレクションの使い分けガイド#
用途に応じた最適なコレクションを選択するためのガイドです。
List実装の選択#
graph TD
A["Listが必要"] --> B{"ランダムアクセス<br/>(インデックスによる取得)<br/>が頻繁?"}
B -->|Yes| C["ArrayList"]
B -->|No| D{"先頭・末尾の<br/>追加削除が頻繁?"}
D -->|Yes| E["LinkedList"]
D -->|No| C
style C fill:#90EE90
style E fill:#87CEEB結論: ほとんどの場合はArrayListを選択してください。LinkedListはメモリ効率が悪く、実務で使用するケースは限定的です。
Set実装の選択#
graph TD
A["Setが必要"] --> B{"ソートが必要?"}
B -->|Yes| C["TreeSet"]
B -->|No| D{"挿入順序を<br/>保持したい?"}
D -->|Yes| E["LinkedHashSet"]
D -->|No| F["HashSet"]
style C fill:#FFB6C1
style E fill:#DDA0DD
style F fill:#90EE90
| 実装 |
順序 |
計算量 |
用途 |
| HashSet |
なし |
O(1) |
高速な重複排除 |
| LinkedHashSet |
挿入順 |
O(1) |
順序を保持した重複排除 |
| TreeSet |
ソート順 |
O(log n) |
ソートされた集合、範囲検索 |
Map実装の選択#
graph TD
A["Mapが必要"] --> B{"キーのソートが必要?"}
B -->|Yes| C["TreeMap"]
B -->|No| D{"挿入順序を<br/>保持したい?"}
D -->|Yes| E["LinkedHashMap"]
D -->|No| F["HashMap"]
style C fill:#FFB6C1
style E fill:#DDA0DD
style F fill:#90EE90
| 実装 |
順序 |
計算量 |
用途 |
| HashMap |
なし |
O(1) |
高速なキー・値検索 |
| LinkedHashMap |
挿入順 |
O(1) |
順序を保持したマップ、LRUキャッシュ |
| TreeMap |
キー順 |
O(log n) |
ソートされたマップ、範囲検索 |
実務でのベストプラクティス#
- インターフェース型で宣言する
1
2
3
4
5
6
7
|
// 推奨:インターフェース型で宣言
List<String> names = new ArrayList<>();
Set<Integer> ids = new HashSet<>();
Map<String, Object> config = new HashMap<>();
// 非推奨:実装クラスで宣言
ArrayList<String> names = new ArrayList<>(); // 避ける
|
- 初期容量を指定する
1
2
3
|
// 大量のデータを格納する場合は初期容量を指定
List<String> largeList = new ArrayList<>(10000);
Map<String, Integer> largeMap = new HashMap<>(10000);
|
- 不変コレクションを活用する
1
2
|
// 変更されないコレクションは不変にする
List<String> constants = List.of("ADMIN", "USER", "GUEST");
|
- 適切な実装を選択する
| 要件 |
推奨実装 |
| 一般的なリスト操作 |
ArrayList |
| 重複排除 |
HashSet |
| 重複排除 + 順序保持 |
LinkedHashSet |
| ソート済みの集合 |
TreeSet |
| キー・値のペア |
HashMap |
| キー・値のペア + 順序保持 |
LinkedHashMap |
| キーでソートされたマップ |
TreeMap |
まとめ#
この記事では、Javaコレクションフレームワークの全体像から、各実装クラスの特徴と使い分け、そしてJava 21のSequenced Collectionsまで解説しました。
主なポイントは以下の通りです。
- List: 順序付きで重複を許可。ほとんどの場合はArrayListを選択
- Set: 重複を許さない集合。HashSetが最も高速、TreeSetでソート
- Map: キーと値のペア。HashMapが最も高速、TreeMapでキーソート
- Sequenced Collections: Java 21で追加された統一APIで先頭・末尾の操作が簡単に
コレクションの特性を理解し、用途に応じて適切な実装を選択することで、効率的で保守しやすいプログラムを作成できます。
参考リンク#