1. コレクションの可変性の問題
Java のコレクションは倉庫のようなものです。誰でも来て何かを追加・削除・変更できます。小規模なら便利ですが、大きなプログラムでは頭痛の種になります。自分のクラスから外部へ商品リストを渡したところ、誰かが半分の項目を削除してしまったと想像してください。あるいは — もっとやっかいなのは — マルチスレッドのプログラムで、あるスレッドが要素を追加し、別のスレッドがそれを読み込む場合です。結果は予期せぬものになり、バグは捕まえにくくなります(例: ConcurrentModificationException)。
なぜ可変コレクションがバグの源泉になるのか、その例です:
import java.util.*;
public class Inventory {
private List<String> products = new ArrayList<>();
public Inventory() {
products.add("お茶");
products.add("コーヒー");
}
public List<String> getProducts() {
// 危険! 内部のリストへの参照を返している
return products;
}
}
public class Main {
public static void main(String[] args) {
Inventory inv = new Inventory();
List<String> external = inv.getProducts();
external.remove("お茶"); // あっ! もう在庫にはお茶がない
System.out.println(inv.getProducts()); // [コーヒー]
}
}
違和感に気づきましたか?一方のメソッドが内部コレクションを返し、別の側がそれを変更しています。これでは、本来保護すべきデータをうっかり破壊してしまいます。
2. 不変コレクションの作成: Collections.unmodifiable*
このような混乱を避けるために、Java には有効な防御策があります。Collections クラスのラッパーでコレクションを「不変」にできます:
- Collections.unmodifiableList(list)
- Collections.unmodifiableSet(set)
- Collections.unmodifiableMap(map)
どう動くのでしょうか。まず通常のコレクションを作り、それを「不変」ラッパーで包みます:
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> drinks = new ArrayList<>();
drinks.add("お茶");
drinks.add("コーヒー");
List<String> immutableDrinks = Collections.unmodifiableList(drinks);
System.out.println(immutableDrinks); // [お茶, コーヒー]
// 追加してみる
immutableDrinks.add("ココア"); // バン! UnsupportedOperationException
}
}
このようなコレクションを変更しようとすると、UnsupportedOperationException が送出されます。まるで箱に大きく "さわるな!" と貼ってあるようなもので、何かを追加・削除しようとするたびに(呼び出しスタックで)手痛いしっぺ返しを食らいます。
例: 内部状態を守る
先ほどの Inventory クラスを修正してみましょう:
import java.util.*;
public class Inventory {
private List<String> products = new ArrayList<>();
public Inventory() {
products.add("お茶");
products.add("コーヒー");
}
public List<String> getProducts() {
// いまはラッパーを返す
return Collections.unmodifiableList(products);
}
}
これで、受け取ったリストを誰かが変更しようとすると例外になります。
3. 不変コレクションの挙動: 表面的な保護
重要なポイント: unmodifiableList などの「仲間」は、元のコレクションの周りに殻をかぶせるだけです。コピーを作るわけではありません — 内部の元コレクションを変更すれば、その変更はラッパー側にも見えます!
デモ
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> drinks = new ArrayList<>();
drinks.add("お茶");
List<String> immutableDrinks = Collections.unmodifiableList(drinks);
drinks.add("コーヒー"); // 元のコレクションを変更
System.out.println(immutableDrinks); // [お茶, コーヒー] — 要素が現れた!
}
}
結論: ラッパーは、そのラッパー経由での変更だけを防ぎます。元のコレクションへの参照を誰かが持っていれば、依然として変更できてしまいます。
4. 深い不変性: 神話と現実
unmodifiable* ラッパーは、コレクションを外側からだけ不変にします。しかし、コレクションに可変オブジェクトが入っていれば、それらは変更できます!
例
import java.util.*;
class Product {
String name;
Product(String name) {
this.name = name;
}
public String toString() {
return name;
}
}
public class Main {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("お茶"));
List<Product> immutableProducts = Collections.unmodifiableList(products);
// コレクション内のオブジェクトを変更
immutableProducts.get(0).name = "コーヒー";
System.out.println(immutableProducts); // [コーヒー]
}
}
結論:
- コレクションは「不変」でも、中のオブジェクトは不変とは限りません。
- 完全(深い)不変性が必要なら、不変オブジェクト(たとえば String、Integer、record クラス)を使うか、自分のクラスを不変にしましょう.
5. 不変コレクションを使うべきとき
内部状態を保護するため
コレクションを保持するクラスを書いていて、それを外部に渡すなら、必ずラッパーを返して、誰にも偶発的(あるいは意図的)にデータを変更させないようにしましょう:
public List<String> getProducts() {
return Collections.unmodifiableList(products);
}
マルチスレッドのプログラムで
マルチスレッドのアプリでは、可変コレクションは問題の温床です(race condition、ConcurrentModificationException など)。作成後に変更する必要がないなら、不変にしておきましょう。
レイヤ間でのデータ受け渡し
アプリのある層から別の層(例: DAO からサービス)にコレクションを渡す場合は、不変コピーやラッパーを渡すと、偶発的な変更から守れます。
6. 実用例
例 1: 学生リストを保護する
import java.util.*;
public class Group {
private final List<String> students = new ArrayList<>();
public void addStudent(String name) {
students.add(name);
}
public List<String> getStudents() {
return Collections.unmodifiableList(students);
}
}
これで、getStudents() 経由で学生を直接追加・削除することはできません。
例 2: 不変なマップ(Map)
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<String, Integer> grades = new HashMap<>();
grades.put("Vasya", 5);
grades.put("Masha", 4);
Map<String, Integer> immutableGrades = Collections.unmodifiableMap(grades);
// immutableGrades.put("Petya", 3); // UnsupportedOperationException
}
}
7. 有用なニュアンス
現代的な代替: List.of, Set.of, Map.of
Java 9 では、不変コレクションをさらに簡単に作れる方法が追加されました:
List<String> drinks = List.of("お茶", "コーヒー");
Set<String> fruits = Set.of("りんご", "バナナ");
Map<String, Integer> ages = Map.of("Vasya", 20, "Masha", 21);
- これらのコレクションは不変です(変更しようとすると例外)。
- 「元の」可変コレクションを持ちません(Collections.unmodifiable* と異なります)。
- null を許可しません。
また、Java 10 には List.copyOf、Set.copyOf、Map.copyOf といったコピー用メソッドがあり、渡されたコレクションの不変コピーを作成します。
不変コレクションの作り方の比較
| 方法 | 深い不変性 | 元のコレクションを変更できるか? | null を許可するか? | Java バージョン |
|---|---|---|---|---|
|
いいえ | はい | はい | 1.2 |
|
いいえ | いいえ(元のコレクションが存在しない) | いいえ | 9+ |
8. 不変コレクションでよくある間違い
誤り No.1: ラッパー作成後に元のコレクションを変更する。 unmodifiableList を作っても、その後で誰かが元のリストを変更してしまう。ラッパーはこれを防げません — 変更はラッパーを使うすべての場所から見えてしまいます。
誤り No.2: 深い不変性を期待してしまう。 コレクションが不変でも、その中のオブジェクトまで変更不可とは限りません。守られるのは構造(追加/削除/コレクション経由での変更)だけで、オブジェクトの中身は別です。
誤り No.3: 近年のファクトリで null を使ってしまう。 List.of、Set.of、Map.of で作られるコレクションは null を許しません。null を追加・取得しようとすると例外になります。
誤り No.4: 可変コレクションへの参照を外部に渡す。 内部コレクションへの参照(ラッパーなし)を返すと、データに対する制御を失い、バグや不変条件の「漏れ」に直結します。
誤り No.5: 変更できる前提のコードに不変コレクションを渡す。 もし外部のコードがコレクションを変更しようとすると(たとえば要素を追加)、UnsupportedOperationException が送出されます。利用側がデータの不変性を理解していることを確認しましょう。
GO TO FULL VERSION