Collections.unmodifiableList仅提供只读视图,不阻止底层列表被其他引用修改;正确做法是从源头确保不可变,如用Arrays.asList+unmodifiableList组合或JDK10+的List.copyOf做浅拷贝。
为什么
不能防住所有修改?
它只提供一层“只读视图”,底层原始
一旦被其他引用持有并修改,包装后的列表会立刻反映出变化——这不是 bug,是设计使然。常见误用是:先创建可变列表,再用
包装,但没切断原始引用。
原始列表如果被保存在某个
字段里,而这个字段又被其他方法直接返回(未包装),那防护就形同虚设
返回的是
(JDK 8+),它不阻止
这类操作,而是抛
,但前提是调用方真的走它的代理方法
若业务代码里用了
得到数组,再转成新
,那就彻底绕过了防护
正确初始化:从源头断掉可变性
最稳妥的做法是让原始数据本身不可变,而不是靠包装拦截。优先用
+
组合,并确保没有中间变量持有可变副本。
别写
——
可能被意外保留
如果配置来自
或 JSON 解析,解析完立即包装,且不要把解析结果赋给非
变量
Spring 等框架注入的
默认是可变的,必须手动包装,不能依赖注入点自动保护
配合
(JDK 10+)更安全
如果你用的是 JDK 10 或更高版本,
是比
更进一步的选择:它不仅不可修改,还会对传入的集合做浅拷贝,切断与原集合的底层数组关联。
要求入参非
,否则抛
;而
会直接 NPE
它内部调用的是
构造器复制元素,所以即使你传入一个后续会被修改的
,
结果也不会受影响
注意:仍是浅拷贝,如果元素本身是可变对象(比如自定义配置类),对象内部状态仍可能被改
警惕泛型擦除导致的运行时绕过
Java 泛型在运行时不存在,所以反射或非类型安全操作仍可能破坏防护。例如:
这种写法在编译期有警告,但不报错;运行时仍会抛异常,说明防护还在起作用
真正危险的是通过反射访问
强制修改内部
字段——
不防御反射,这是 JVM 层面的限制
如需强隔离,考虑用
(Guava)或
(JDK 9+ 的
),它们连反射修改内部数组都更难成功
真正的防护不在一层包装,而在初始化路径是否干净、引用是否收敛、以及是否接受“不可变”语义的完整约束。稍有松动,比如留了一个可变的 setter 或日志打印时转成了
,整个防线就失效了。
Collections.unmodifiableListListunmodifiableListprivate final List configList unmodifiableListUnmodifiableRandomAccessListset(0, "x")UnsupportedOperationExceptionlist.toArray()ArrayListArrays.asListunmodifiableListpublic static final List ALLOWED_TYPES =
Collections.unmodifiableList(Arrays.asList("http", "https", "file")); List tmp = new ArrayList<>(); tmp.add(...); return Collections.unmodifiableList(tmp); tmpPropertiesfinal@Value("${my.list}") ListcopyOfList.copyOfunmodifiableListpublic static final List ROLES = List.copyOf(
Arrays.asList("admin", "user", "guest")); List.copyOfnullNullPointerExceptionunmodifiableList(null)ArrayList::newArrayListcopyOfList rawList = (List) ALLOWED_TYPES; // 擦除后变成原始类型
rawList.set(0, "ftp"); // 运行时不会报错,但会触发 UnsupportedOperationExceptionfield.setAccessible(true)listunmodifiableListImmutableList.ofjava.util.ImmutableCollections$ListNList.ofnew ArrayList