跳转到主内容
websoft网络软件专家 - 深耕网络技术,打造实用软件!

怎么利用 Collections.unmodifiableList 包装集合防止业务配置被意外修改

Collections.unmodifiableList仅提供只读视图,不阻止底层列表被其他引用修改;正确做法是从源头确保不可变,如用Arrays.asList+unmodifiableList组合或JDK10+的List.copyOf做浅拷贝。 为什么
Collections.unmodifiableList
不能防住所有修改? 它只提供一层“只读视图”,底层原始
List
一旦被其他引用持有并修改,包装后的列表会立刻反映出变化——这不是 bug,是设计使然。常见误用是:先创建可变列表,再用
unmodifiableList
包装,但没切断原始引用。 原始列表如果被保存在某个
private final List configList
字段里,而这个字段又被其他方法直接返回(未包装),那防护就形同虚设
unmodifiableList
返回的是
UnmodifiableRandomAccessList
(JDK 8+),它不阻止
set(0, "x")
这类操作,而是抛
UnsupportedOperationException
,但前提是调用方真的走它的代理方法 若业务代码里用了
list.toArray()
得到数组,再转成新
ArrayList
,那就彻底绕过了防护 正确初始化:从源头断掉可变性 最稳妥的做法是让原始数据本身不可变,而不是靠包装拦截。优先用
Arrays.asList
+
unmodifiableList
组合,并确保没有中间变量持有可变副本。
public static final List ALLOWED_TYPES = Collections.unmodifiableList(Arrays.asList("http", "https", "file"));
别写
List tmp = new ArrayList<>(); tmp.add(...); return Collections.unmodifiableList(tmp);
——
tmp
可能被意外保留 如果配置来自
Properties
或 JSON 解析,解析完立即包装,且不要把解析结果赋给非
final
变量 Spring 等框架注入的
@Value("${my.list}") List
默认是可变的,必须手动包装,不能依赖注入点自动保护 配合
copyOf
(JDK 10+)更安全 如果你用的是 JDK 10 或更高版本,
List.copyOf
是比
unmodifiableList
更进一步的选择:它不仅不可修改,还会对传入的集合做浅拷贝,切断与原集合的底层数组关联。
public static final List 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
,整个防线就失效了。

相关文章