Java 逆引き集 | Immutable wrappers と defensive copy パターン — API 安全性

Java Java
スポンサーリンク

Immutable wrappers と defensive copy パターン — API 安全性

API 設計で「外部から渡されたコレクションや配列を勝手に書き換えられてしまう」問題を防ぐために使うのが Immutable wrappersdefensive copy
初心者が理解しやすいように、例題とテンプレートで整理します。


基本の考え方

  • Immutable wrappers(不変ラッパー):
    • Collections.unmodifiableListList.copyOf などを使って「読み取り専用ビュー」を返す。
    • 呼び出し側が add/remove しようとすると UnsupportedOperationException が出る。
    • 元のリストを変更するとビューにも反映される点に注意。
  • Defensive copy(防御的コピー):
    • 外部から渡された配列やリストを「コピー」して内部に保持。
    • 外部が後から変更しても内部状態は影響を受けない。
    • 返すときもコピーを返すことで「内部状態を守る」。

基本コード例

Immutable wrappers

import java.util.*;

public class ImmutableDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(List.of("A", "B", "C"));

        List<String> unmod = Collections.unmodifiableList(list);
        System.out.println(unmod); // [A, B, C]

        // unmod.add("X"); // 実行すると UnsupportedOperationException
        list.add("X"); // 元リストを変更するとビューにも反映
        System.out.println(unmod); // [A, B, C, X]
    }
}
Java

Defensive copy

import java.util.*;

final class User {
    private final List<String> tags;

    User(List<String> tags) {
        // 外部から渡されたリストをコピーして保持
        this.tags = new ArrayList<>(tags);
    }

    public List<String> getTags() {
        // 内部リストを直接返さずコピーを返す
        return new ArrayList<>(tags);
    }
}

public class DefensiveDemo {
    public static void main(String[] args) {
        List<String> src = new ArrayList<>(List.of("java", "api"));
        User u = new User(src);

        src.add("hack"); // 外部リストを変更
        System.out.println(u.getTags()); // [java, api] (影響なし)

        List<String> got = u.getTags();
        got.add("evil"); // 返されたコピーを変更
        System.out.println(u.getTags()); // [java, api] (内部は守られる)
    }
}
Java

例題で理解する

例題1: API が返す設定リストを不変にする

class ConfigService {
    private final List<String> endpoints = new ArrayList<>(List.of("api1","api2"));

    public List<String> getEndpoints() {
        return Collections.unmodifiableList(endpoints);
    }
}
Java
  • ねらい: 呼び出し側が勝手に add/remove できないようにする。

例題2: コンストラクタで defensive copy

class Order {
    private final Date created;

    Order(Date created) {
        // Date は可変なのでコピーして保持
        this.created = new Date(created.getTime());
    }

    public Date getCreated() {
        // 内部状態を守るためコピーを返す
        return new Date(created.getTime());
    }
}
Java
  • ねらい: Date のような可変オブジェクトを安全に扱う。

例題3: Immutable wrappers と defensive copy の組み合わせ

class Library {
    private final List<String> books;

    Library(List<String> books) {
        // defensive copy
        this.books = new ArrayList<>(books);
    }

    public List<String> getBooks() {
        // 不変ビューを返す
        return Collections.unmodifiableList(books);
    }
}
Java
  • ねらい: 内部状態を守りつつ、外部からは読み取り専用で見せる。

テンプレート集

  • 不変ビューを返す
return Collections.unmodifiableList(list);
return Collections.unmodifiableSet(set);
return Collections.unmodifiableMap(map);
Java
  • コピーして保持
this.field = new ArrayList<>(inputList);
this.field = Arrays.copyOf(inputArray, inputArray.length);
Java
  • コピーして返す
return new ArrayList<>(field);
return Arrays.copyOf(field, field.length);
Java
  • Java 9+ の簡潔な不変コピー
List<String> safe = List.copyOf(inputList);
Set<String> safeSet = Set.copyOf(inputSet);
Java

落とし穴と回避策

  • Immutable wrappers は「ビュー」: 元リストを変更するとビューにも反映される。完全に独立させたいなら defensive copy。
  • 返すときに直接内部を渡す: 内部状態が外部から変更される危険。必ずコピーか不変ビューを返す。
  • 可変型(Date, Calendar, 配列): 特に defensive copy が必須。
  • パフォーマンス: 大量データで毎回コピーするとコスト増。必要に応じてキャッシュや unmodifiableView を選ぶ。

まとめ

  • Immutable wrappers: 外部からの「書き換え」を禁止するビュー。
  • Defensive copy: 外部から渡された可変オブジェクトをコピーして内部を守る。返すときもコピー。
  • 組み合わせ: 内部は defensive copy、外部には immutable view を返すのが API 安全設計の定石。

👉 練習課題として「学生クラスに List<String> subjects を持たせ、コンストラクタで defensive copy、getter で unmodifiableList を返す」コードを書いてみると理解が深まります。

タイトルとURLをコピーしました