Java 深拷贝(Deep Copy)与浅拷贝(Shallow Copy)
核心概念回顾
- 浅拷贝 (Shallow Copy): 创建一个新对象,新对象的非静态字段的值与原始对象完全相同。
- 如果字段是基本类型 (如
int,double,char),则直接复制其值。 - 如果字段是引用类型 (如对象、数组),则复制的是该字段的内存地址(引用)。因此,新对象和原始对象的这个字段指向堆内存中的同一个实际对象。
- 结果: 新对象和原始对象在基本类型字段上是独立的,但在引用类型字段上是共享的。修改其中一个对象的引用类型字段内容,会影响到另一个对象。
- 如果字段是基本类型 (如
- 深拷贝 (Deep Copy): 创建一个新对象,并递归地复制原始对象及其所有引用的对象。
- 对于基本类型字段,直接复制值。
- 对于引用类型字段,创建一个新的对象(或数组),并将原始对象中对应字段所引用对象的内容完整复制到这个新对象中。新对象中的引用字段指向的是全新的、内容相同的对象,而不是原始对象中的那个对象。
- 结果: 新对象和原始对象完全独立,互不影响。修改任何一个对象的任何字段(基本类型或引用类型的内容),都不会影响另一个对象。
对比与区别
| 特性 | 浅拷贝 (Shallow Copy) | 深拷贝 (Deep Copy) |
|---|---|---|
| 复制对象 | 只复制对象本身(第一层) | 复制对象本身及其引用的所有对象(递归整个对象图) |
| 引用字段 | 复制引用(内存地址),指向同一个对象实例 | 创建新对象实例,指向内容相同但独立的新实例 |
| 独立性 | 引用类型字段不独立(共享对象) | 所有字段(基本类型和引用类型)完全独立 |
| 内存开销 | 较小(只创建新对象实例,不创建引用对象的新实例) | 较大(创建新对象实例及其所有引用对象的新实例) |
| 性能 | 较快(仅复制一层) | 较慢(需要递归复制整个对象图) |
| 实现难度 | 相对简单(通常只需实现 Cloneable + clone()) | 相对复杂(需要递归处理所有引用对象) |
| 修改影响 | 修改引用对象内容会影响原对象和新对象 | 修改新对象的任何内容都不会影响原对象 |
| 典型实现 | Object.clone() (默认行为) | 手动递归 clone()、序列化/反序列化、第三方库 |
原理与源码级别分析
-
Object.clone()方法:浅拷贝的基石- Java 中所有类的基类
Object提供了一个protected native Object clone() throws CloneNotSupportedException;方法。 native关键字: 表明该方法的具体实现是由底层的 JVM(通常用 C/C++ 编写)完成的,不是用 Java 代码写的。我们无法直接看到其 Java 源码。- 默认行为(浅拷贝): JVM 实现的
clone()方法默认执行以下操作:- 为新对象分配内存空间(大小与原对象相同)。
- 将原对象的内存布局(即所有字段的值)按位复制(bitwise copy) 到新分配的内存中。
- 对于基本类型字段:值被直接复制。
- 对于引用类型字段:引用值(内存地址) 被直接复制。这就是浅拷贝的根源。
Cloneable接口: 仅仅是一个标记接口(Marker Interface),没有任何方法。一个类需要实现这个接口,以表明它允许被克隆。如果一个类没有实现Cloneable却调用了clone(),会抛出CloneNotSupportedException。- 覆盖
clone(): 要使用clone()方法,类通常需要:- 实现
Cloneable接口。 - 覆盖
Object.clone()方法,并将其访问修饰符改为public。 - 在覆盖的方法中调用
super.clone()来获取浅拷贝的副本。
public class MyClass implements Cloneable { private int value; private SomeObject ref; // 引用类型字段 @Override public Object clone() throws CloneNotSupportedException { return super.clone(); // 默认浅拷贝 } }- 上面的
clone()方法执行的就是浅拷贝。ref字段在新旧对象中指向同一个SomeObject实例。
- 实现
- Java 中所有类的基类
-
实现深拷贝
- 由于
Object.clone()默认是浅拷贝,要实现深拷贝,需要在覆盖的clone()方法中手动处理所有引用类型字段的复制。 - 递归克隆: 这是最直接的方式,要求引用类型字段所属的类也正确实现了
clone()(深拷贝或浅拷贝视需求而定)。public class DeepCopyClass implements Cloneable { private int value; private SomeObject ref; // 假设 SomeObject 也实现了 Cloneable 和 clone() @Override public Object clone() throws CloneNotSupportedException { DeepCopyClass copy = (DeepCopyClass) super.clone(); // 先做浅拷贝 // 对引用字段进行深拷贝:创建新对象并复制内容 copy.ref = (SomeObject) ref.clone(); // 递归调用 ref 的 clone() return copy; } }- 关键点:
copy.ref = (SomeObject) ref.clone();这行代码确保了ref字段指向的是原始ref对象的一个副本,而不是原始对象本身。 - 要求:
SomeObject类必须正确实现clone()(通常是深拷贝实现)。如果SomeObject内部还有引用字段,它也需要递归处理它们。这种方式要求整个对象图上的类都支持clone()。
- 关键点:
- 序列化/反序列化: 另一种常见的深拷贝实现方式。
import java.io.*; public class DeepCopyViaSerialization { public static T deepCopy(T object) throws IOException, ClassNotFoundException { // 序列化对象到字节数组 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(object); oos.flush(); oos.close(); // 从字节数组反序列化出新对象 ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return (T) ois.readObject(); } }- 原理: 将对象及其引用的整个对象图序列化(转换为字节流),然后再反序列化(从字节流重建对象)。这个过程会重建所有对象,因此结果是深拷贝。
- 要求: 对象及其所有引用的对象都必须实现
java.io.Serializable接口。 - 优缺点: 实现简单,不要求对象图上的每个类都实现
clone()。但性能通常比递归clone()差,且依赖于序列化机制。
- 第三方库: Apache Commons Lang 的
SerializationUtils.clone()(基于序列化),或 Gson/Jackson 等 JSON 库(对象->JSON->对象)也可以实现深拷贝,各有优缺点(性能、灵活性、对构造函数的处理等)。
- 由于
应用分析
-
何时使用浅拷贝:
- 对象内部状态完全由不可变对象(Immutable Objects)构成: 例如
String,Integer,BigDecimal等。因为不可变对象本身无法修改,共享它们是安全的。String的拷贝通常就是浅拷贝(因为字符串常量池和不可变性)。 - 性能敏感且引用对象无需独立: 如果明确知道引用字段不会被修改,或者多个对象共享同一个大对象是设计所需(节省内存),且这种共享不会导致问题。
clone()默认行为: 当类的字段都是基本类型或不可变引用类型时,直接使用super.clone()是高效且安全的。- 示例: 某些配置对象(只读)、享元模式(Flyweight)中的共享对象。
- 对象内部状态完全由不可变对象(Immutable Objects)构成: 例如
-
何时使用深拷贝:
- 需要完全独立的对象副本: 这是最常见的原因。例如:
- 原型模式(Prototype Pattern): 通过复制现有对象(原型)来创建新对象,新对象必须独立于原型。
- 避免副作用: 将对象作为参数传递给方法,但又不希望方法内部修改影响原始对象(防御性拷贝 - Defensive Copying)。集合类(如
Collections.unmodifiableList)返回的视图有时会进行浅拷贝的防御性拷贝,但如果要完全隔离,需要深拷贝。 - 多线程环境: 将对象状态复制一份给另一个线程处理,确保线程间数据隔离。
- 缓存或快照: 保存对象在某个时间点的完整状态。
- 复杂对象图的复制: 如复制一个包含订单、订单项、客户信息的完整订单对象。
- 引用对象的状态可变: 如果类包含的引用字段指向的对象是可变的(Mutable),并且你希望副本与原对象在这些字段上完全独立,则必须深拷贝。
- 示例: 游戏中的角色状态保存/加载、复杂配置对象的模板、工作流中传递需要隔离的数据包。
- 需要完全独立的对象副本: 这是最常见的原因。例如:
重要注意事项与陷阱
clone()方法设计缺陷: Java 的clone()机制常被认为设计不佳。- 它破坏了构造函数封装(
clone()不通过构造函数创建对象)。 Cloneable是一个标记接口,但clone()却在Object中,导致语义不清晰。- 深拷贝实现需要手动处理,容易出错(忘记拷贝某个字段、循环引用导致无限递归或栈溢出)。
- 许多 JDK 类(如
ArrayList,HashMap)实现了clone()作为浅拷贝。使用时务必查阅文档。
- 它破坏了构造函数封装(
- 循环引用: 深拷贝时,如果对象图存在循环引用(A 引用 B,B 引用 A),递归
clone()可能导致无限递归或栈溢出。序列化方式通常能更好地处理循环引用(通过维护对象引用的映射)。 - 性能考量: 深拷贝的成本可能很高,尤其是对于大型或深层次的对象图。在性能关键路径上需谨慎使用。
- 构造函数逻辑:
clone()和序列化都不调用构造函数。如果对象构造时有重要的初始化逻辑(写在构造函数里),这些逻辑在拷贝过程中会被跳过,可能导致问题。需要确保这些逻辑对于拷贝后的对象状态不是必需的,或者在clone()方法/反序列化回调(readObject)中手动执行。 final字段:clone()机制可以修改final字段的值(因为 JVM 在底层直接复制内存),这违反了final的常规语义(通常只能在构造函数中赋值)。序列化/反序列化也会重新设置final字段的值。这是一个需要注意的角落案例。- 替代方案: 由于
clone()的问题,很多情况下更推荐:- 复制构造函数(Copy Constructor):
public MyClass(MyClass other) { ... } - 复制工厂方法(Copy Factory Method):
public static MyClass newInstance(MyClass other) { ... } - 这些方式更符合面向对象的设计原则(使用构造函数),也更清晰、更灵活(可以返回子类实例等)。
- 复制构造函数(Copy Constructor):
总结
理解 Java 中深拷贝与浅拷贝的区别至关重要,它直接关系到程序的正确性(对象状态的独立性)和性能。Object.clone() 提供了浅拷贝的基础,但实现深拷贝需要开发者额外的工作(递归克隆或序列化)。在选择拷贝策略时,务必考虑对象引用的可变性、对独立性的需求以及性能开销。在 clone() 机制存在缺陷的情况下,复制构造函数或工厂方法通常是更推荐的对象复制方式。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120530

浙公网安备 33010602011771号