bigywater.tech
Java 内部原理 · 27

Collections.unmodifiableList 真实只是一层视图:改原始 list,"不可变"的那份也真实跟着变

"unmodifiableList 返回的是不可变集合"这句话只对了一半——它确实不允许你通过这个返回值去修改内容,但它拦不住原始集合被改,而且改了之后"不可变"的那份会真实同步跟着变。

original 改了 → 视图跟着变
真实验证:original.add("d") 之后,拿到的 unmodifiable 引用内容真实同步变成了包含 "d" 的新内容
add() → 真实抛异常
直接对 unmodifiable 调用 add() 真实抛出 UnsupportedOperationException——挡得住这个方向的修改,挡不住那个方向

上一篇:Arrays.asList() 真实是个固定大小的视图:改它真的会改到原始数组

真实验证:改原始 list,"不可变"视图真实跟着变

Collections.unmodifiableList(original) 返回的是一个包装类,内部持有的就是 original 这个引用本身,每次访问都真实转发到 original 上。

UnmodifiableViewDemo.java ArrayList<String> original = new ArrayList<>(List.of("a", "b", "c")); List<String> unmodifiable = Collections.unmodifiableList(original); original.add("d"); // 只改了 original,没碰 unmodifiable
真实实测
unmodifiable = [a, b, c]

original.add("d") 之后:
  original      = [a, b, c, d]
  unmodifiable  = [a, b, c, d]("不可变"视图是否也真的变了?)

original.remove("a") 之后:
  original      = [b, c, d]
  unmodifiable  = [b, c, d]

真实结果很直接:代码里从来没有对 unmodifiable 这个变量做过任何操作,但它每次被打印出来的内容都真实和 original 保持同步——因为它本来就不是一份独立的数据,只是一个"这里不允许你改"的访问权限限制,包在同一份底层数据外面。

真实验证:直接改视图本身,确实会抛异常

unmodifiableList 真实起作用的地方,是拦截"通过这个引用发起的修改"。

真实实测
unmodifiable.add("z") 真实抛出 UnsupportedOperationException

这一点符合预期,也是这个方法名字的字面含义所在——但真实验证下来,它保护的范围比很多人以为的要窄:它只保证"你没法通过这个 unmodifiable 变量修改内容",完全不保证"这份数据不会变"。如果调用方以为拿到一个 unmodifiableList 就等于拿到了一份安全的、不会被意外修改的快照,而背后还有别的代码持有 original 的引用并继续修改它,这个"不可变"的假设就是错的。

真实验证:想要真正的快照,得先拷贝

正确做法是先用拷贝构造函数复制一份独立数据,再包装成不可变视图。

真实实测
先拷贝(new ArrayList<>(original))再包装成 unmodifiableList 之后,再改 original:
  original     = [b, c, d, e]
  realSnapshot = [b, c, d](这次才真的没受影响,因为拷贝出了独立的底层数据)
这次真实做到了完全隔离——realSnapshot 内部持有的是一份独立分配的数组,和 original 之后无论怎么增删,两者都真实互不影响
Java 9+ 的 List.of() 能替代这个模式吗?能,而且更彻底。List.of(...) 创建的是真正不可变的实现(内部数据在构造时就固定,类型上也和普通 ArrayList 完全不同),没有"包一层视图"这个中间状态,天然不存在这篇文章里验证的这个陷阱。如果目标就是要一份真正安全的不可变快照,List.copyOf(original)(内部等价于拷贝一份再返回不可变实现)是比 Collections.unmodifiableList(new ArrayList<>(original)) 更直接的写法。

分步演示:哪些改动会同步,哪些不会

每一步的内容都是真实运行这段代码之后的状态。

original 改了,unmodifiable 真实跟着变;先拷贝再包装,才是真正独立的快照
这一步真实发生变化的位置

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到。
    • 和Arrays.asList 那篇是"视图 vs 拷贝"这同一个主题的两个例子,建议对照读。
    • 没有涉及:Collections.unmodifiableMap/unmodifiableSet 等其它集合类型的同类包装方法——原理完全一致,这里不重复验证。
    京ICP备2026067502号-1京公网安备11010502063487号