Java中clone()默认为浅拷贝,仅复制引用;深拷贝需递归创建新对象,可通过重写clone()、序列化、JSON转换或构造函数实现,但各有局限与性能代价。

浅拷贝只是复制引用,不是复制对象本身
Java 中的 clone() 方法默认实现的是浅拷贝:它会创建一个新对象,但所有引用类型的字段仍指向原对象中相同的堆内存地址。这意味着修改副本里的某个可变对象(比如 ArrayList、自定义类实例),原对象也会被影响。
常见错误现象:
Person p1 = new Person("Alice", new Address("Beijing"));
Person p2 = (Person) p1.clone();
p2.getAddress().setCity("Shanghai");
System.out.println(p1.getAddress().getCity()); // 输出 "Shanghai" —— 原对象被意外改了
关键判断点:只要类里有非基本类型字段(String 除外,因不可变),且没重写 clone() 做深度处理,就一定是浅拷贝。
使用场景:适合只读或字段全是基本类型/不可变对象(如 String、LocalDateTime)的类;也常用于性能敏感、明确不需要隔离引用的临时副本。
深拷贝必须递归复制所有引用对象
深拷贝要求每个嵌套对象都新建实例,并逐层复制内容。Java 没有语言级支持,必须手动实现或借助工具。
立即学习“Java免费学习笔记(深入)”;
实操建议:
- 重写
clone():在子类中对每个引用字段调用其clone()(需确保它们也实现了Cloneable) - 使用序列化:把对象写入
ByteArrayOutputStream再反序列化,能自动穿透所有层级(但要求所有字段类型都可序列化,且含transient字段时需额外处理) - 用 Jackson/Gson:将对象转成 JSON 字符串再解析回新对象(注意:仅适用于 POJO,且会丢失方法、泛型类型信息)
- 构造函数注入:为每个嵌套对象提供带参数的构造器,显式 new 出新实例(最可控,但代码量大)
性能影响:深拷贝必然比浅拷贝慢,尤其对象图复杂或含大集合时;序列化方式还涉及 IO 和反射开销。
Arrays.copyOf() 和 ArrayList.clone() 都是浅拷贝
很多人误以为 Arrays.copyOf() 或 new ArrayList(oldList) 是深拷贝,其实它们只复制了数组/列表容器本身,内部元素仍是原引用。
示例:
Listlist1 = Arrays.asList(new Person("Tom"), new Person("Jerry")); List list2 = new ArrayList<>(list1); list2.get(0).setName("Bob"); // list1.get(0) 名字也被改成 "Bob"
正确做法:
- 若元素类型支持
clone():用stream().map(Person::clone).collect(Collectors.toList()) - 若元素是简单 POJO:用
list1.stream().map(p -> new Person(p.getName())).collect(...) - 避免直接用
ArrayList.clone()——它返回Object,还需强转,且仍是浅拷贝
特别注意:String、Integer 等包装类和不可变类,即使在浅拷贝中也不会出问题,因为它们无法被“修改”。
Serializable 方式深拷贝容易忽略的坑
用序列化做深拷贝看似方便,但实际踩坑率很高:
- 所有嵌套类(包括内部类、Lambda 捕获的对象)都必须实现
Serializable,否则抛NotSerializableException -
transient字段不会被序列化,拷贝后为 null 或默认值,需在readObject()中手动恢复 - 静态字段不参与序列化,所以拷贝前后共享同一份静态状态
- 某些 JDK 类(如
Thread、Socket)不可序列化,含这类字段的类无法用此法 - 序列化机制依赖类的
serialVersionUID,版本不一致可能反序列化失败
如果只是为了深拷贝而给大量业务类加 implements Serializable,往往得不偿失。更稳妥的方式是只对明确需要深拷贝的小范围模型类启用该方案,并配单元测试验证字段完整性。
真正复杂的对象图,手动控制拷贝逻辑反而更清晰、更易调试、更容易绕过不可序列化的组件。










