bigywater.tech
Java 内部原理 · 62

Object.clone() 浅拷贝真实验证:数组和集合字段真实共享引用

"clone() 默认是浅拷贝"这句话人人都知道,但真正踩过坑的人才知道"浅"到什么程度。这篇真实用一个带数组、集合、字符串字段的类,一项一项验证 clone() 之后哪些字段是独立的、哪些是共享的。

数组字段真实共享同一引用
真实验证:original.arr == copy.arr 结果是 true,clone() 只复制了引用本身,没有复制数组内容
修改副本真实影响原对象
真实验证:copy.arr[0]=999 之后,original.arr[0] 真实也变成了 999,这是浅拷贝最直接的后果

上一篇:注解 RetentionPolicy 真实验证

真实验证:哪些字段是独立的,哪些是共享的

用 super.clone() 默认实现,对比基本类型对应的包装/String字段、数组字段、集合字段克隆后的真实引用关系。

CloneShallowDemo.java class CloneShallowDemo implements Cloneable { int[] arr = {1, 2, 3}; List<String> list = new ArrayList<>(Arrays.asList("a", "b")); String name = "original"; public CloneShallowDemo clone() { return (CloneShallowDemo) super.clone(); } }
真实实测
original == copy: false
original.name == copy.name (String不可变,共享无所谓): true
original.arr == copy.arr (数组字段): true
original.list == copy.list (集合字段): true

真实结果验证了 super.clone() 的默认行为:它真实创建了一个新对象(original == copy 为 false),但对每个字段只做了"按位复制"——对于引用类型字段,复制的是引用本身(内存地址),不是引用指向的对象。name 字段虽然也是共享的引用,但因为 String 不可变,共享并不会引发问题;而 arr 和 list 这两个可变对象的引用被真实共享,才是浅拷贝真正危险的地方。

真实验证:修改副本的可变字段,原对象真实受影响

直接修改 copy 的数组元素和集合内容,观察 original 是否真实跟着变化。

真实实测
修改 copy.arr[0]=999 后,original.arr[0] = 999
修改 copy.list.add(c) 后,original.list = [a, b, c]
浅拷贝的真实代价。这两行结果是浅拷贝最直观的证据:代码逻辑上只操作了 copy 这一个"副本",但因为 arr 和 list 在两个对象之间真实是同一份引用,任何修改都会真实反映到 original 身上——这在需要"独立副本"的场景下(比如给每个请求发一份配置对象的克隆,防止互相污染)是一个真实存在、很容易被忽视的 bug 来源。

真实验证:手动深拷贝数组字段之后,两者才真正独立

在 clone() 方法内部,对数组字段再手动调用一次 clone(),验证这样是否真的能让两个对象互不影响。

CloneShallowDemo.java static class DeepDemo implements Cloneable { int[] arr = {1, 2, 3}; public DeepDemo clone() { DeepDemo d = (DeepDemo) super.clone(); d.arr = arr.clone(); // 手动深拷贝数组字段 return d; } }
真实实测
深拷贝版本 d1.arr == d2.arr: false
修改 d2.arr[0]=999 后,d1.arr[0] = 1(深拷贝后互不影响)

真实结果证明了正确的做法:在 clone() 方法内部,对每一个可变引用类型字段手动调用一次它自己的 clone()(数组的 clone() 本身就是深拷贝一层),让 d1.arr 和 d2.arr 真实变成两个独立的数组对象。这一次修改 d2.arr[0],d1.arr[0] 真实保持不变——这也是很多 IDE 自动生成的 clone() 只做了 super.clone() 却没有处理引用字段,导致"看起来实现了 Cloneable,实际上是个陷阱"的根本原因。

分步演示:共享的引用编号,和分开之后的编号

用 identityHashCode 给每个数组/集合对象编号,串成动画:编号相同就是同一个对象,共享;编号不同就是两个独立对象,互不影响。

clone() → 改 copy 的字段 → 换成手动深拷贝:引用编号真实是怎么共享又怎么分开的
这一步被赋值/展示的字段

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,浅拷贝/深拷贝对照结果重复运行完全一致。
    • 和Record 类那篇一起看,能理解为什么现代 Java 更倾向于用不可变对象(Record)从根本上规避浅拷贝陷阱,而不是依赖 Cloneable。
    • 没有涉及:Cloneable 接口本身不含任何方法、clone() 真实定义在 Object 里且是 protected native 方法的设计争议(被 Josh Bloch 在《Effective Java》里明确建议避免使用)——这个话题值得单独写。
    京ICP备2026067502号-1京公网安备11010502063487号