Java 逆引き集 | Collections.unmodifiableList 等(読み取り専用ラッパー) — API 返却時の安全性

Java Java
スポンサーリンク

Collections.unmodifiableList 等(読み取り専用ラッパー) — API 返却時の安全性

外部にコレクションを返すとき「勝手に書き換えられない」ようにするのが読み取り専用ラッパー。Collections.unmodifiableXxx を使えば、既存のコレクションを「読み取り専用ビュー」に包んで安全に渡せます。初心者でも失敗しない使い方、落とし穴、テンプレートをまとめます。


目的と基本原則

  • 目的: 返却したコレクションを呼び出し側から変更させない(API境界の安全性向上)。
  • 仕組み: 元コレクションを「変更禁止ビュー」で包む。読み取りは可能、変更操作は例外になる。
  • 注意: ラッパーは「ビュー」。元コレクションが変わると、ビューも変わって見える。

代表的なラッパーと使い方

  • List の読み取り専用ビュー
import java.util.*;

List<String> modifiable = new ArrayList<>();
modifiable.add("A"); modifiable.add("B");

// 読み取り専用ビューで包む
List<String> readOnly = Collections.unmodifiableList(modifiable);

// 読み取りはOK
System.out.println(readOnly.get(0)); // A

// 変更は例外(UnsupportedOperationException)
readOnly.add("C");      // 例外
readOnly.remove("A");   // 例外
Java
  • Set / Map の読み取り専用ビュー
Set<Integer> s = new HashSet<>(List.of(1, 2, 3));
Set<Integer> roSet = Collections.unmodifiableSet(s);

Map<String, Integer> m = new HashMap<>();
m.put("A", 1); m.put("B", 2);
Map<String, Integer> roMap = Collections.unmodifiableMap(m);
Java
  • ネスト構造の例(Map<String, List<String>)
Map<String, List<String>> data = new HashMap<>();
data.put("fruits", new ArrayList<>(List.of("Apple", "Banana")));

// まず内側の List を読み取り専用ビューに
data.replaceAll((k, v) -> Collections.unmodifiableList(v));
// 次に外側の Map を読み取り専用ビューに
Map<String, List<String>> ro = Collections.unmodifiableMap(data);
Java

よくある落とし穴と回避策

  • 落とし穴1: 元コレクションが変更されるとビューも変わる
    • 回避: API返却前に「防御的コピー」を取り、コピーを包んで返す。
public List<String> getNames() {
    // 内部状態をコピーしてから包む
    return Collections.unmodifiableList(new ArrayList<>(this.names));
}
Java
  • 落とし穴2: 参照先要素が可変(浅い読み取り専用)
    • 説明: ラッパーはコレクション構造の変更を禁じるだけ。要素オブジェクトが可変なら、要素の中身は書き換え可能。
    • 回避: 要素を不変(Immutable)にする、または要素も防御的コピーして返す。
  • 落とし穴3: null をそのまま包む
    • 症状: Collections.unmodifiableList(null) は NullPointerException。
    • 回避: nullの場合は Collections.emptyList()Collections.emptySet() を返す。
public List<String> safe(List<String> in) {
    return (in == null) ? Collections.emptyList()
                        : Collections.unmodifiableList(new ArrayList<>(in));
}
Java
  • 落とし穴4: キャストで外して変更しようとする
    • 説明: ラッパー結果は実装が異なるため、元の型へのキャストは失敗(ClassCastException)。
    • 姿勢: 「外して変更」を前提にしない設計にする。変更したければ「明示的なコピー」を作る。
  • 落とし穴5: スレッド安全と誤解
    • 説明: 読み取り専用ビューは「変更操作を拒否」するだけで、並行アクセスを同期しない。
    • 回避: マルチスレッドで共有するなら、公開前に完全に構築し、その後は変更しないか、スレッド安全な構造を使う。

Java 9+ の代替(本当に不変なコレクション)

  • 固定不変コレクション:List.of(...), Set.of(...), Map.of(...)
    • 特徴: 中身もサイズも変更不可。配列やリストを包むビューではなく、真に不変なインスタンス。
List<String> names = List.of("A", "B", "C"); // 変更操作は常に例外
Set<Integer> flags = Set.of(1, 2, 3);
Map<String, Integer> score = Map.of("A", 10, "B", 20);
Java
  • 使い分け:
    • 返却時に「その場で固定」したい → of(...)
    • 既存の可変コレクションを「読み取り専用ビュー化」したい → unmodifiableXxx

API設計のテンプレートとベストプラクティス

返却時の基本テンプレート(防御的コピー+読み取り専用)

public final class Order {
    private final List<String> items;

    public Order(List<String> items) {
        // 内部に取り込む時点でコピー
        this.items = new ArrayList<>(Objects.requireNonNull(items));
    }

    // 返却はコピー+読み取り専用ビュー
    public List<String> items() {
        return Collections.unmodifiableList(new ArrayList<>(items));
    }
}
Java

null セーフ返却テンプレート

public List<String> tagsOrEmpty() {
    return (tags == null) ? Collections.emptyList()
                          : Collections.unmodifiableList(new ArrayList<>(tags));
}
Java

ネスト構造の防御的コピー

public Map<String, List<String>> snapshot() {
    Map<String, List<String>> copy = new HashMap<>();
    for (var e : this.data.entrySet()) {
        copy.put(e.getKey(), new ArrayList<>(e.getValue())); // 内側コピー
    }
    return Collections.unmodifiableMap(copy); // 外側包む
}
Java

不変+読み取り専用の組み合わせ(推奨)

public record Item(String id, String name) { } // 要素を不変に

// 不変要素を含むリストを返却
public List<Item> getItems() {
    return Collections.unmodifiableList(new ArrayList<>(items));
}
Java

例題で理解する

  • 例題1: 設定値の公開(外から変更禁止)
public final class AppConfig {
    private final Map<String, String> props = new HashMap<>();

    public AppConfig(Map<String, String> initial) {
        props.putAll(initial); // 取り込み時コピー
    }

    public Map<String, String> properties() {
        return Collections.unmodifiableMap(new HashMap<>(props)); // 返却時コピー+包む
    }
}
Java
  • 例題2: 監査ログ出力用のスナップショット
public class Audit {
    private final List<String> events = new ArrayList<>();

    public synchronized void record(String e) {
        events.add(e);
    }

    public synchronized List<String> snapshot() {
        // 一時的にコピーしてから読み取り専用を返す
        return Collections.unmodifiableList(new ArrayList<>(events));
    }
}
Java
  • 例題3: Java 9+ 固定リストで返却
public List<String> builtins() {
    return List.of("start", "stop", "status"); // 真に不変
}
Java

実務での指針

  • 返却は「変更できない形」が基本: API境界の安全性・予測可能性が上がる。
  • 防御的コピーを優先: 内部状態を直接晒さない。ビューは元が変わると追随するため、返却前にコピー。
  • 要素も不変に: 可能なら Immutable な要素で安全性を高める。
  • Java 9+ を活用: 固定の初期データは List.of などで簡潔に不変化。
  • パフォーマンス配慮: 大量データで毎回コピーが重いなら、用途に応じてスナップショットの頻度や粒度を調整。

まとめ

  • Collections.unmodifiableXxx は「読み取り専用ビュー」を作る仕組み。変更操作は例外になるが、元コレクションに追随する点に注意。
  • API返却時は「防御的コピー+読み取り専用」をセットにすると安全。要素の不変化と Java 9+ の of 系も併用すると堅牢性が上がる。
  • スレッド安全とは別問題。並行環境では公開前に構築を完了させ、返却は不変・読み取り専用で扱うのが基本。

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