bigywater.tech
Java 内部原理 · 105

Collections.nCopies() 真实验证:三个位置真实是同一个对象,不是三份拷贝

Collections.nCopies(n, obj) 看起来像是"生成n份obj的副本",这篇真实验证它到底是不是这个意思——对于可变对象,这个理解真实是错的,而且错得很危险。

三个位置真实是同一引用
真实验证:points.get(0) == points.get(1) 真实为true,nCopies()真实只是把同一个引用重复了n次
修改共享对象真实全部同步变化
真实验证:修改这个共享对象一次,列表所有位置真实同步跟着变化,因为指向的是同一份数据

上一篇:Files.walk() 真实验证

真实验证:对可变对象,nCopies真实是同一引用重复N次

对不可变的String,nCopies()看起来完全没问题;换成一个可变对象,立刻能看出真实机制。

CollectionsNCopiesDemo.java MutablePoint shared = new MutablePoint(1); List<MutablePoint> points = Collections.nCopies(3, shared); points.get(0) == points.get(1);
真实实测
nCopies(3, sharedObj) 真实结果: [P(1), P(1), P(1)]
points.get(0) == points.get(1) 真实结果: true(真实是同一个对象,不是拷贝)

真实结果证明了 nCopies() 的真实实现方式:它内部真实只保存了一份对象引用和一个数量 n,get(i)(不管 i 是多少)真实都返回同一个对象。这和"创建 n 份独立拷贝"是完全不同的语义——它更接近"把同一个东西在逻辑上重复展示 n 次"。

真实验证:修改共享对象,所有位置真实同步变化

既然是同一个引用,修改这个对象一次,理论上列表所有位置读出来的都应该跟着变——真实验证一下。

真实实测
修改shared.x=999后,points真实结果: [P(999), P(999), P(999)](三个位置真实同步变化,因为都指向同一个对象)
修改sbList.get(0)的StringBuilder后,sbList.get(1) 真实结果: init-modified(同样真实被污染,因为是同一个StringBuilder对象)
真实的危险场景。这是 nCopies() 最容易被误用的地方:很多人想用 Collections.nCopies(3, new StringBuilder()) 或类似写法快速初始化"3个独立的可变容器"(比如给3个统计维度各自准备一个累加器),真实结果却是所有位置指向同一个对象——只要修改了其中一个"位置",其他所有"位置"看起来也真实跟着变了,而这个 bug 往往要等到运行时数据出现诡异的全局同步现象才会被发现,debug 起来非常反直觉。

真实验证:返回的List本身真实不可变

真实实测
真实验证:nCopies()返回的List真实不支持set(),抛出UnsupportedOperationException

真实结果证明 nCopies() 返回的 List 本身是只读的——不能用 set() 替换某个位置指向的对象,这一点和系列前面 List.of() 那篇验证过的不可变集合行为一致。但要注意这个"不可变"只限定在"列表结构本身不能修改"这个层面,不代表列表里那个被共享的对象内容也不可变——如果这个共享对象自身是可变类型,它的内部状态依然真实可以被修改,进而影响到所有"位置"的读取结果。

真实验证:正确的初始化写法

真实实测
正确写法:修改independent.get(0)后,independent.get(1) 真实结果: P(0)(真实互不影响)

真实结果验证了正确的做法:用循环真实创建 n 个独立的 new 对象逐个添加进列表,而不是依赖 nCopies()。nCopies() 真正适合的场景是那些"数量固定、值本身不可变或不需要独立修改"的用途,比如快速生成一个占位符列表(Collections.nCopies(n, "-"))或者作为某个 API 需要固定长度 List 参数时的简便写法。

分步演示:三个"位置"到底是不是三个对象

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

nCopies(3, shared) 的三个位置真实是同一个对象;换成循环 new 三次,才是三个真正独立的对象
这一步涉及的位置

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,共享引用行为重复运行完全一致。
    • 和Object.clone() 浅拷贝那篇一起看,能理解"共享引用导致意外联动"这个主题在Java里反复出现的不同场景——从clone()到数组、再到集合工厂方法,本质都是同一条规律。
    • 没有涉及:Arrays.fill()对对象数组填充时是否存在同样的共享引用陷阱(结论是存在,原理完全一致)、Stream.generate()配合Supplier真实创建独立对象的正确写法——这两个话题留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号