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));
}
}
Javanull セーフ返却テンプレート
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系も併用すると堅牢性が上がる。 - スレッド安全とは別問題。並行環境では公開前に構築を完了させ、返却は不変・読み取り専用で扱うのが基本。
