Collections.unmodifiableList の役割をざっくりつかむ
Collections.unmodifiableList は、
「この List、“見たり読むのはいいけど、書き換えは禁止ね”という“読み取り専用ビュー”を作るメソッド」
です。
ポイントは「コピーを作る」のではなく、「元の List をラップして“変更禁止の顔”をかぶせる」ことです。
返ってきた List に対して add や remove を呼ぶと、UnsupportedOperationException という実行時例外が投げられます。
「中身を勝手に変更されたくないけど、一覧としては渡したい」
こういうときに、とても強力な安全装置になります。
まずは基本:動きが一目で分かるサンプル
変更禁止ビューを作る基本例
次のコードを見てください。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class UnmodifiableListBasic {
public static void main(String[] args) {
List<String> modifiable = new ArrayList<>();
modifiable.add("A");
modifiable.add("B");
List<String> readOnly = Collections.unmodifiableList(modifiable);
System.out.println("元: " + modifiable); // [A, B]
System.out.println("ビュー: " + readOnly); // [A, B]
readOnly.add("C"); // ここで UnsupportedOperationException
}
}
JavareadOnly に対して add("C") を呼んだ瞬間、例外が発生します。
これが「変更禁止ビュー」である証拠です。
しかし、get や size などの「読むだけ」のメソッドは普通に使えます。
System.out.println(readOnly.get(0)); // "A"(OK)
System.out.println(readOnly.size()); // 2(OK)
Java「読むだけはOK、変更はNG」という性質を持った List を作るのが、Collections.unmodifiableList です。
「コピーではない」という点をしっかり理解する
元の List を変えると、ビュー側にも反映される
ここが一番誤解されやすいポイントです。
unmodifiableList が返すのは「スナップショットのコピー」ではなく、「元の List をそのまま覗いている窓」です。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class UnmodifiableListView {
public static void main(String[] args) {
List<String> modifiable = new ArrayList<>();
modifiable.add("A");
modifiable.add("B");
List<String> readOnly = Collections.unmodifiableList(modifiable);
System.out.println(readOnly); // [A, B]
modifiable.add("C"); // 元のリストを変更
System.out.println(readOnly); // [A, B, C] ← ちゃんと増えている
}
}
JavareadOnly 自体は変更できませんが、「元のリスト側の変更」は丸見えです。
だから、“完全コピー”が欲しい場合には使ってはいけない、ということも分かります。
「今この瞬間の内容を凍結したスナップショット」が欲しければ、
List<String> snapshot = new ArrayList<>(modifiable); // 中身をコピー
Javaのように、自分でコピー用の List を作る必要があります。unmodifiableList はあくまで「変更禁止ビュー」であって、「固定した内容のコピー」ではありません。
なぜわざわざ変更禁止ビューを作るのか(設計の話)
メソッドの「戻り値」を安全にしたい
たとえば、クラスの内部で持っている List を返すメソッドを考えます。
class UserGroup {
private final List<String> members = new ArrayList<>();
public List<String> getMembers() {
return members; // そのまま返している
}
}
Javaこの設計だと、呼び出し側が
UserGroup group = new UserGroup();
// ... どこかでメンバーを追加したりしているとする
List<String> list = group.getMembers();
list.clear(); // 全部消せてしまう
list.add("謎のメンバー"); // 勝手に追加もできる
Javaというコードを書けてしまいます。
つまり、「UserGroup の内部状態を外部から自由に壊せる」状態です。
これを防ぐために、
class UserGroup {
private final List<String> members = new ArrayList<>();
public List<String> getMembers() {
return Collections.unmodifiableList(members);
}
}
Javaとしておくと、呼び出し側が
group.getMembers().add("X");
group.getMembers().clear();
Javaのように呼んだ瞬間、UnsupportedOperationException が出ます。
つまり、
「中身を見る権利はあげるけど、構造を変える権利はあげない」
という明確な境界線を引けるわけです。
バグを「起こさせない」ための防御
現場では、「間違えて書き換えてしまう」バグを未然に防ぐために、unmodifiableList がよく使われます。
自分ひとりで書いている小さいプログラムならまだしも、
大人数で長期間メンテするコードでは、
「この List、外から書き換えられていいのか、ダメなのか」
が曖昧だと、どこかで必ず事故ります。
外から書き換えてほしくないなら、Collections.unmodifiableList で「API の顔」として明示しておく。
これは、設計としてかなり大事な考え方です。
読み取り専用でも「中身のオブジェクト」は変えられることに注意
シャロー(浅い)読み取り専用である、という感覚
unmodifiableList が禁止しているのは、
List の構造の変更(add, remove, clear, set など)
です。
しかし、List の要素がオブジェクトの場合、
そのオブジェクトの中身まで不変になるわけではありません。
import java.util.*;
class User {
String name;
User(String name) { this.name = name; }
@Override
public String toString() { return name; }
}
public class UnmodifiableListShallow {
public static void main(String[] args) {
List<User> users = new ArrayList<>();
users.add(new User("Alice"));
users.add(new User("Bob"));
List<User> readOnly = Collections.unmodifiableList(users);
// readOnly.add(new User("Carol")); // ← これは例外(構造の変更)
User u = readOnly.get(0);
u.name = "ALICE!!"; // 要素オブジェクトの中身は普通に書き換えられる
System.out.println(readOnly); // [ALICE!!, Bob]
}
}
JavaList の「枠組み」は変更できませんが、
中に入っているオブジェクトが可変なら、その中身は普通に変わります。
つまり、
「読み取り専用なのは List の構造までであって、中身のオブジェクトの不変性までは保証しない」
ということです。
完全に不変にしたいなら、
イミュータブルなオブジェクト(フィールドを変更不能にする)を要素に使う
あるいは、コピーしたオブジェクトを中に入れる
といった別の工夫も必要です。
似た仲間たち:他の unmodifiable 系との関係
Set / Map にも同じものがある
Collections クラスには、List 以外にも同じコンセプトのメソッドがあります。
Collections.unmodifiableSet(set)Collections.unmodifiableMap(map)
などです。
どれも考え方は同じで、
元のコレクションをラップして、構造変更メソッドを禁止する読み取り専用ビューを作る
という振る舞いです。
API 設計の観点では、
List を返すなら unmodifiableList
Set を返すなら unmodifiableSet
Map を返すなら unmodifiableMap
という選択肢を常に意識しておくと、
「境界の安全性」がぐっと高まります。
まとめ:Collections.unmodifiableList をどう頭に置いておくか
初心者向けにまとめると、Collections.unmodifiableList はこういうものです。
- 元の List をラップして、「変更禁止の読み取り専用ビュー」を返すメソッド。
- コピーではなくビューなので、元の List を変えるとビュー側にも反映される。
- ビューに対して add / remove / clear / set などの構造変更メソッドを呼ぶと UnsupportedOperationException。
- 外部に「中身は見せたいが、勝手に書き換えられたくない」List を渡すときの防御手段としてとても重要。
- List の構造は不変になるが、要素オブジェクト自体の中身は(可変なら)普通に変更できる。
