CodeGym /コース /JAVA 25 SELF /不変コレクション: Collections.unmodifiable

不変コレクション: Collections.unmodifiable

JAVA 25 SELF
レベル 33 , レッスン 2
使用可能

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); // [コーヒー]
    }
}

結論:

  • コレクションは「不変」でも、中のオブジェクトは不変とは限りません。
  • 完全(深い)不変性が必要なら、不変オブジェクト(たとえば StringIntegerrecord クラス)を使うか、自分のクラスを不変にしましょう.

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.copyOfSet.copyOfMap.copyOf といったコピー用メソッドがあり、渡されたコレクションの不変コピーを作成します。

不変コレクションの作り方の比較

方法 深い不変性 元のコレクションを変更できるか? null を許可するか? Java バージョン
Collections.unmodifiableList(list)
いいえ はい はい 1.2
List.of(...), Set.of(...), Map.of(...)
いいえ いいえ(元のコレクションが存在しない) いいえ 9+

8. 不変コレクションでよくある間違い

誤り No.1: ラッパー作成後に元のコレクションを変更する。 unmodifiableList を作っても、その後で誰かが元のリストを変更してしまう。ラッパーはこれを防げません — 変更はラッパーを使うすべての場所から見えてしまいます。

誤り No.2: 深い不変性を期待してしまう。 コレクションが不変でも、その中のオブジェクトまで変更不可とは限りません。守られるのは構造(追加/削除/コレクション経由での変更)だけで、オブジェクトの中身は別です。

誤り No.3: 近年のファクトリで null を使ってしまう。 List.ofSet.ofMap.of で作られるコレクションは null を許しません。null を追加・取得しようとすると例外になります。

誤り No.4: 可変コレクションへの参照を外部に渡す。 内部コレクションへの参照(ラッパーなし)を返すと、データに対する制御を失い、バグや不変条件の「漏れ」に直結します。

誤り No.5: 変更できる前提のコードに不変コレクションを渡す。 もし外部のコードがコレクションを変更しようとすると(たとえば要素を追加)、UnsupportedOperationException が送出されます。利用側がデータの不変性を理解していることを確認しましょう。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION