Java面试之深拷贝与浅拷贝的区别

浅拷贝只复制对象本身而不复制引用指向的堆内存,导致新旧对象共享引用对象;深拷贝需递归复制所有层级引用,确保完全独立。

浅拷贝只复制对象本身,不复制引用指向的堆内存

当你调用 Object.clone() 且该类没重写 clone() 或没实现 Cloneable,会抛出 CloneNotSupportedException。即使成功,默认行为是浅拷贝:基本类型字段值被复制,引用类型字段只复制地址,新旧对象共享同一堆内存中的对象。

常见错误现象:修改克隆后对象的某个 List 元素,原对象也跟着变;toString() 输出看似不同,但 == 比较引用字段返回 true

  • 必须显式实现 Cloneable 接口(仅作标记,无方法)
  • protectedclone() 需改为 public 并处理异常
  • 对每个可变引用字段(如 ArrayList、自定义对象),要手动调用其 clone() 或新建实例并复制内容

深拷贝要递归复制所有层级的引用对象

深拷贝的目标是让克隆对象与原对象完全独立,任意一方修改内部状态都不影响另一方。没有语言级内置支持,必须手动实现或借助工具。

使用场景包括:

缓存中返回对象副本避免污染、多线程间安全传递可变对象、测试中隔离 fixture 状态。

  • 手动实现:重写 clone(),对每个引用字段 new 一个新对象,并递归调用其 clone()(要求它们也支持)
  • 序列化方式(如 ObjectOutputStream + ByteArrayInputStream):要求所有字段类型都实现 Serializable,且注意 transient 字段丢失、性能差、无法处理循环引用
  • JSON 序列化(如 Jackson):简单但有类型擦除风险(泛型信息丢失)、不支持非 public 字段、忽略 transient 和静态字段

为什么 Arrays.copyOf()new ArrayList(list) 不是深拷贝

这些操作常被误认为“深拷贝”,其实只是对容器本身做了浅层复制 —— 新建了数组或 ArrayList 实例,但其中元素仍是原引用。

String[] arr1 = {"a", "b"};
String[] arr2 = Arrays.copyOf(arr1, arr1.length);
arr2[0] = "x"; // ✅ arr1[0] 还是 "a",String 不可变,看不出来
List list1 = Arrays.asList(new StringBuilder("hello"));
List list2 = new ArrayList<>(list1);
list2.get(0).append("!"); // ❌ list1.get(0) 也会变成 "hello!"
  • Arrays.copyOf() 复制的是数组引用本身,不是元素内容
  • new ArrayList(collection) 调用的是 addAll(),本质是遍历赋值引用
  • 只有元素是不可变对象(如 StringInteger)时,浅拷贝“看起来”像深拷贝

面试中容易被追问的边界点

面试官常从实现细节切入,比如:如果对象里有 final 字段、有 ThreadLocal、含 Lambda 表达式、或继承自第三方类无法修改源码,怎么办?

  • final 引用字段在浅拷贝中无法重新赋值,必须在构造时初始化,深拷贝需通过反射绕过(不推荐)或改用工厂方法
  • ThreadLocal 是线程绑定的,不应被拷贝;若强行序列化会失效,应明确设计为“不参与拷贝”
  • Lambda 表达式编译后是私有静态方法+捕获变量,序列化可能失败;建议避免在需深拷贝的对象中持有 Lambda
  • 无法修改父类时,子类 clone() 中对父类引用字段的深拷贝逻辑必须小心:不能访问 super.clone() 返回对象的私有字段

真正难的不是写出一个能跑的 clone 方法,而是判断哪些字段必须深拷、哪些可以共享、哪些根本不该拷贝 —— 这取决于业务语义,不是技术规则能覆盖的。