Collections.unmodifiableList仅提供只读视图,不阻止底层列表被其他引用修改;正确做法是从源头确保不可变,如用Arrays.asList+unmodifiableList组合或JDK10+的List.copyOf做浅拷贝。

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

















