1. はじめに
従来のコレクション: 柔軟性と落とし穴
new ArrayList<>() を使ってコレクションを作成すると、要素の追加・削除・変更を自由に行える構造になります。データをその場で組み立てるときは便利です。ですが、そのコレクションを変更してほしくない別のクラスやメソッドに渡したらどうなるでしょう? うっかり外部に渡して誰かに変更されてしまったら? ここから問題が始まります。
典型的なミスの例
import java.util.*;
public class Example {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
// コレクションを「外」に渡す
processNames(names);
// リストが変わっていないと期待している…
System.out.println(names);
}
public static void processNames(List<String> list) {
// しかし誰かが要素を削除してしまった!
list.remove("Bob");
}
}
出力:
[Alice, Charlie]
意図していないのにコレクションが変わってしまいました。大規模なプロジェクトでは、このような「思わぬ事態」が非常に厄介で捕まえにくいバグへと発展しがちです。
危険なのは、コレクションを扱うコードがデータの予測不能な変更に突然遭遇し得る点です。さらに、情報を失うリスクも常にあります — 誰かが誤って要素を削除したり上書きしたりするかもしれません。もしコレクションを複数のスレッドから同時に変更すれば、ConcurrentModificationException だけでなく、さらに厄介な問題 — 不整合なデータ — に直面する可能性があります。
コレクションの保護: 従来のアプローチ
Java 9 以前は、前のレベルでも話したように Collections.unmodifiableList(...) のようなラッパーメソッドを使う必要がありました。これは「凍結された」コレクションを返すのに役立ちます。ただしこの方法は常に便利とは限らず、すべての問題を解決するわけでもありません(詳しくは次の講義で)。
2. 現代的な解決策: ファクトリメソッド List.of、Set.of、Map.of
Java 9 ではコレクションのインターフェースに新しい静的メソッド List.of、Set.of、Map.of が追加されました。これらは、変更できないコレクションを手早く簡単に作成できます。たとえるなら、コレクションを作ってすぐコンクリートで固めるようなもの — 誰も要素の追加・削除・変更ができません。
不変コレクションの作成例
import java.util.*;
public class ImmutableDemo {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie");
Set<Integer> numbers = Set.of(1, 2, 3);
Map<String, Integer> ages = Map.of("Alice", 30, "Bob", 25, "Charlie", 28);
System.out.println(names);
System.out.println(numbers);
System.out.println(ages);
}
}
出力:
[Alice, Bob, Charlie]
[1, 2, 3]
{Alice=30, Bob=25, Charlie=28}
どのように動作するのか?
- List.of(...) — 不変のリストを作成する。
- Set.of(...) — 不変のセットを作成する。
- Map.of(...) — 不変のマップを作成する(最大 10 組のキーと値。より多い場合は Map.ofEntries(...) を使用)。
注意! これらのメソッドで作成したコレクションは変更不可です。要素の追加・削除・置換を行おうとすると必ず例外が送出されます。
3. 使用例と「落とし穴」
例: コレクションを変更しようとする
import java.util.*;
public class ImmutableFail {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob");
// names.add("Charlie"); // 実行時エラー!
try {
names.add("Charlie");
} catch (UnsupportedOperationException ex) {
System.out.println("要素を追加できない: " + ex.getClass().getSimpleName());
}
}
}
出力:
要素を追加できない: UnsupportedOperationException
例: null を追加しようとする
import java.util.*;
public class NullFail {
public static void main(String[] args) {
try {
List<String> badList = List.of("Alice", null, "Bob");
} catch (NullPointerException ex) {
System.out.println("Null は禁止: " + ex.getClass().getSimpleName());
}
}
}
出力:
Null は禁止: NullPointerException
例: Set.of の重複
import java.util.*;
public class DuplicatesFail {
public static void main(String[] args) {
try {
Set<String> badSet = Set.of("one", "two", "one");
} catch (IllegalArgumentException ex) {
System.out.println("重複は禁止: " + ex.getClass().getSimpleName());
}
}
}
出力:
重複は禁止: IllegalArgumentException
例: 多数のペアを持つ Map.of
import java.util.*;
public class MapOfLarge {
public static void main(String[] args) {
// Map.of はキーと値のペアを最大 10 組までサポート
Map<String, Integer> map = Map.of(
"one", 1, "two", 2, "three", 3, "four", 4, "five", 5,
"six", 6, "seven", 7, "eight", 8, "nine", 9, "ten", 10
);
System.out.println(map);
// より多い場合は Map.ofEntries を使う
Map<String, Integer> bigMap = Map.ofEntries(
Map.entry("eleven", 11),
Map.entry("twelve", 12),
Map.entry("thirteen", 13)
// ...など
);
System.out.println(bigMap);
}
}
4. 不変コレクションの特徴と制約
変更不可。
要素の追加・削除・変更を試みると、必ず UnsupportedOperationException が送出されます。通常は許可されているメソッド(add、remove、set)も使えません。
null は使用不可。
null をリストやセットの要素として、あるいはマップのキー/値として追加しようとすると NullPointerException になります。これは安全性のためです。null 要素はコレクションでしばしば不具合の原因になります。
具体的な実装は保証されない。
List.of などで作成されたコレクションの裏で、どのクラスが使われているかは分かりません。instanceof で ArrayList を期待したり、特定の実装型にキャストしようとするのはやめましょう。
要素の順序。
— List.of は要素の順序が保持されます(通常のリストと同様)。
— Set.of は順序が保証されません(実際には引数の順と一致することもありますが、当てにしないでください)。
— Map.of はエントリの順序が保証されません。
性能。
ファクトリメソッドで作成されたコレクションは、可変コレクションのラッパーよりも高速なことが多いです。余分な機能のためのメモリを消費しないためです。
5. 不変コレクションを使う場面と理由
定数データセット
アプリの実行中に変更されるべきでないリスト・セット・マップがあるなら、List.of、Set.of、Map.of を使いましょう。例えば:
private static final List<String> ROLES = List.of("USER", "ADMIN", "MODERATOR");
このリストに余計なロールを誰も追加できなくなります。
メソッドからコレクションを返すとき
メソッドからコレクションを返し、呼び出し側に変更されたくない場合:
public List<String> getDefaultNames() {
return List.of("Alice", "Bob", "Charlie");
}
受け取った側はあなたのデータを壊せません。
アプリケーション層間の受け渡し
プログラムの異なる部分(例えば Web アプリケーションの Controller 層と Service 層)間でコレクションを受け渡しするときは、不変コレクションを使うのがよいでしょう。誰にもこっそり変更させないためです。
安全性とスレッドセーフ
不変コレクションは定義上、読み取りに関してはスレッドセーフです。誰も変更できないため、同期なしで複数スレッドから安心して読み取れます。
6. 汎用アプリ向けの実用例
例えば、学習用アプリにサポートしているコマンドの一覧があるとします:
public class Commands {
public static final List<String> SUPPORTED_COMMANDS = List.of(
"help", "exit", "list", "add", "remove"
);
}
次のようにしようとすると:
Commands.SUPPORTED_COMMANDS.add("hack_the_system");
例外が送出され、アプリに害を与えることはできません。
あるいは、エラーコードのマップがあるとします:
public class ErrorCodes {
public static final Map<Integer, String> CODES = Map.of(
404, "Not Found",
500, "Internal Server Error",
403, "Forbidden"
);
}
新しいコードを追加しようとすると、例外が送出されます。
7. コレクション生成アプローチの比較
| 作成方法 | 変更可能? | null 可? | 重複? | スレッドセーフ | 例 |
|---|---|---|---|---|---|
|
はい | はい | はい | いいえ | |
|
いいえ | いいえ | はい | はい* | |
|
いいえ | いいえ | いいえ | はい* | |
|
いいえ | いいえ | いいえ | はい* | |
|
いいえ | 元のコレクションに依存 | はい | いいえ | |
* — スレッドセーフなのは不変であるという側面に限られます。誰もコレクションを変更しないなら、複数スレッドから安全に読み取れます。
8. List.of, Set.of, Map.of の典型的な誤り
エラー1: コレクションを変更しようとする。
とてもよくあるのが、List.of、Set.of、Map.of で作成したコレクションに要素を追加・削除しようとすることです。例えば names.add("Dmitry") や ages.remove("Bob")。これは常に実行時に UnsupportedOperationException になります。
エラー2: null を追加しようとする。
うっかりどれかのメソッドに null を渡すと(例: List.of("Alice", null))、NullPointerException になります。Java 9+ の不変コレクションは null を受け付けません — 実はこのほうが健全です。
エラー3: Set.of や Map.of に重複要素。
Set.of("a", "b", "a") や Map.of("x", 1, "x", 2) は IllegalArgumentException になります。セットやマップは定義上、重複を含められません。
エラー4: 具体的な実装を期待する。
次のように書くべきではありません:
List<String> list = List.of("a", "b");
if (list instanceof ArrayList) {
// ...
} // これは常に false です!
内部実装は隠蔽されています — 実装詳細に依存しないでください。
エラー5: 変更系メソッドを使おうとする。
clear() や(リストの)set(index, value) のようなメソッドでさえ例外を送出します。ファクトリメソッドで作られたコレクションは 不変 であることを忘れないでください。
GO TO FULL VERSION