Collections.unmodifiableList返回的是原列表的只读代理视图而非新列表,读操作委托原列表,写操作抛UnsupportedOperationException;它不复制数据,故原列表修改会实时反映在视图中。
为什么
返回的不是新列表而是视图
不复制元素,只包装原
对象,返回一个代理对象。所有读操作(如
、
)委托给底层列表,所有写操作(如
、
、
)直接抛出
。
这意味着:如果原始列表后续被修改,不可修改视图会立刻反映这些变化;反之,试图通过视图修改会立即失败。
适合场景:需要临时暴露一个列表供外部只读访问,且你仍控制原始列表生命周期
不适合场景:想彻底冻结数据快照(此时应先
再包装)
注意:它不阻止对列表中可变对象的内部修改(例如列表里是
,仍可调用其
)
如何避免“假只读”:原始列表泄露问题
常见错误是把私有字段直接传给
后返回,但调用方可能偷偷保留原始引用:
正确做法是确保原始列表不被外部持有:
构造时用防御性拷贝:
再包装
或在类初始化后不再暴露原始引用(例如用
字段 + 构造器注入,且不提供 setter)
若原始列表来自外部输入,务必先拷贝:
与
的关键区别
JDK 9+ 提供的
创建的是真正不可变(immutable)实例,底层无修改入口、不可继承、序列化安全;而
只是运行时拦截写操作的包装器。
:空实现、无 backing list、
等直接返回常量,性能略优
:有实际 backing list,内存占用稍高,且若原始
被反射篡改(极端情况),视图可能异常
兼容性:
不支持
元素;
继承原始列表的
容忍性
嵌套结构下只读不等于深层只读
如果你的列表里存的是自定义对象(比如
),
只锁住列表结构,不影响
实例自身的状态变更:
要真正“冻结”内容,需配合不可变类型(如使用
、或手动实现不可变
类),或在封装层做深拷贝 ——
本身不处理这一层。
最容易被忽略的一点:只读列表的“只读性”完全依赖于你是否切断了所有通往原始列表的引用路径,以及其中元素是否自身可变。这两层都得自己兜底。
Collections.unmodifiableListCollections.unmodifiableListListget()size()add()set()clear()UnsupportedOperationExceptionnew ArrayList(original)StringBuilderappend()unmodifiableListprivate List data = new ArrayList<>();
public List getData() {
return Collections.unmodifiableList(data); // ❌ data 仍可从别处被修改
} new ArrayList(source)finalCollections.unmodifiableList(new ArrayList(input))unmodifiableListList.of()List.of()unmodifiableListList.of("a", "b")size()Collections.unmodifiableList(new ArrayList<>(Arrays.asList("a","b")))ArrayListList.of()nullunmodifiableListnullUserunmodifiableListUserList users = new ArrayList<>();
users.add(new User("Alice"));
List roUsers = Collections.unmodifiableList(users);
roUsers.get(0).setName("Bob"); // ✅ 合法!User 对象本身仍可变 recordUserunmodifiableList