浅拷贝,深拷贝
下面我们来详细讲解一下 Java 中的深拷贝和浅拷贝。在 Java 中,这两个概念主要出现在对象复制的过程中,理解它们对于编写健壮的代码至关重要。
一、概念回顾
在 Java 中,一切皆对象。变量分为两种:
- 基本数据类型:
int
,double
,char
,boolean
等。它们存储的是“值”本身。 - 引用数据类型:
String
, 数组,以及所有自定义的类(如Person
,ArrayList
等)。它们存储的不是对象本身,而是指向堆内存中对象的“引用”(可以理解为内存地址)。
浅拷贝 和 深拷贝 的核心区别就在于,当复制一个包含引用类型成员的对象时,如何处理这些引用。
二、浅拷贝
1. 定义
浅拷贝会创建一个新对象,然后将原对象的字段值逐个复制到新对象中。
- 如果字段是基本数据类型,复制的是它的值。修改副本不会影响原对象。
- 如果字段是引用数据类型,复制的是它的引用(内存地址)。这意味着,原对象和副本对象将共享同一个引用指向的对象。
2. 效果
对副本对象中引用类型成员的修改,会直接反映到原对象上,因为它们操作的是同一个内存中的对象。
3. Java 中的实现方式
a. Object.clone()
方法
Java 中所有类的父类 Object
提供了一个 protected native Object clone()
方法。要使用它,需要满足两个条件:
- 实现
Cloneable
接口:这是一个标记接口,没有任何方法,只是告诉 JVM 这个对象是可被克隆的。如果不实现,调用clone()
会抛出CloneNotSupportedException
。 - 重写
clone()
方法:将Object
类中protected
的clone()
方法重写为public
,以便外部类可以调用。
b. 示例代码
让我们定义一个 Person
类,它包含一个基本类型 age
和一个引用类型 Address
。
// Address.java - 一个引用类型
class Address {String city;public Address(String city) {this.city = city;}@Overridepublic String toString() {return "Address{city='" + city + "'}";}
}// Person.java - 包含基本类型和引用类型
class Person implements Cloneable {private int age;private Address address; // 引用类型public Person(int age, Address address) {this.age = age;this.address = address;}// Getter 和 Setterpublic int getAge() {return age;}public void setAge(int age) {this.age = age;}public Address getAddress() {return address;}public void setAddress(Address address) {this.address = address;}@Overridepublic String toString() {return "Person{age=" + age + ", address=" + address + "}";}// 重写 clone() 方法实现浅拷贝@Overridepublic Person clone() {try {// 直接调用 Object 的 clone() 方法return (Person) super.clone();} catch (CloneNotSupportedException e) {// 因为实现了 Cloneable 接口,所以这个异常理论上不会发生throw new AssertionError();}}
}
c. 测试浅拷贝
public class ShallowCopyDemo {public static void main(String[] args) {// 1. 创建原始对象Address address = new Address("北京");Person person1 = new Person(30, address);// 2. 执行浅拷贝Person person2 = person1.clone();System.out.println("--- 修改前 ---");System.out.println("Person1: " + person1);System.out.println("Person2: " + person2);System.out.println("Person1.address == Person2.address ? " + (person1.getAddress() == person2.getAddress()));System.out.println("\n--- 修改副本 person2 的基本类型 age ---");person2.setAge(31);System.out.println("Person1: " + person1); // Person1 的 age 未改变System.out.println("Person2: " + person2);System.out.println("\n--- 修改副本 person2 的引用类型 address ---");// 注意:我们是通过 person2 拿到它内部的 address 对象,然后修改这个对象的 city 属性person2.getAddress().city = "上海";System.out.println("Person1: " + person1); // Person1 的 address.city 也被改变了!System.out.println("Person2: " + person2);}
}
d. 输出结果与分析
--- 修改前 ---
Person1: Person{age=30, address=Address{city='北京'}}
Person2: Person{age=30, address=Address{city='北京'}}
Person1.address == Person2.address ? true--- 修改副本 person2 的基本类型 age ---
Person1: Person{age=30, address=Address{city='北京'}}
Person2: Person{age=31, address=Address{city='北京'}}--- 修改副本 person2 的引用类型 address ---
Person1: Person{age=30, address=Address{city='上海'}}
Person2: Person{age=31, address=Address{city='上海'}}
分析:
person1.address == person2.address
返回true
,有力地证明了它们共享同一个Address
对象。- 修改
person2
的基本类型age
,person1
不受影响。 - 修改
person2
的引用类型address
的内部属性city
,person1
的address.city
也随之改变。这就是浅拷贝的“副作用”。
三、深拷贝
1. 定义
深拷贝不仅会创建一个新对象,还会递归地复制对象内部的所有引用类型成员,直到所有对象都是全新的副本。
- 对于基本数据类型,行为和浅拷贝一样,复制值。
- 对于引用数据类型,会创建一个全新的对象,并将新对象的引用赋值给副本。
2. 效果
原对象和副本对象之间没有任何共享。对副本的任何修改都不会影响到原对象,反之亦然。它们是完全独立的两个对象。
3. Java 中的实现方式
a. 重写 clone()
方法(手动实现)
这是最原始的方式,但也是最繁琐、最容易出错的方式。你需要确保所有包含引用类型的成员变量也都实现了 Cloneable
接口,并在其 clone()
方法中进行深拷贝。
修改 Address
和 Person
类:
// Address.java - 也需要实现 Cloneable
class Address implements Cloneable {String city;// ... 构造函数和 toString() 同上 ...@Overridepublic Address clone() {try {return (Address) super.clone();} catch (CloneNotSupportedException e) {throw new AssertionError();}}
}// Person.java - 修改 clone() 方法
class Person implements Cloneable {// ... 其他成员和方法同上 ...// 重写 clone() 方法实现深拷贝@Overridepublic Person clone() {try {Person person = (Person) super.clone();// 关键:对引用类型的成员,也调用其 clone() 方法,创建一个新对象person.address = this.address.clone(); // 深拷贝的核心return person;} catch (CloneNotSupportedException e) {throw new AssertionError();}}
}
测试:使用上面同样的测试代码,你会发现输出结果完全不同:
--- 修改前 ---
Person1: Person{age=30, address=Address{city='北京'}}
Person2: Person{age=30, address=Address{city='北京'}}
Person1.address == Person2.address ? false // <-- 关键变化!--- 修改副本 person2 的基本类型 age ---
Person1: Person{age=30, address=Address{city='北京'}}
Person2: Person{age=31, address=Address{city='北京'}}--- 修改副本 person2 的引用类型 address ---
Person1: Person{age=30, address=Address{city='北京'}} // <-- Person1 不再受影响
Person2: Person{age=31, address=Address{city='上海'}}
缺点:如果对象层级很深,或者包含集合、数组等复杂结构,你需要为每一层、每一个元素都实现 clone()
,代码会变得非常冗长和难以维护。
b. 序列化方式(推荐)
这是一种更通用、更健壮的实现深拷贝的方式。其原理是:将对象序列化(写入)到一个字节流中,然后再从字节流中反序列化(读出)来,得到一个全新的对象。
要求:对象及其内部所有引用类型的成员都必须实现 Serializable
接口。
实现一个工具类:
import java.io.*;public class DeepCopyUtil {@SuppressWarnings("unchecked")public static <T extends Serializable> T deepCopy(T object) {try {// 1. 将对象序列化到字节数组输出流ByteArrayOutputStream baos = new ByteArrayOutputStream();try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {oos.writeObject(object);}// 2. 从字节数组输入流中反序列化出对象try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) {return (T) ois.readObject();}} catch (IOException | ClassNotFoundException e) {throw new RuntimeException("Failed to deep copy object", e);}}
}
修改 Address
和 Person
类:
// 只需要实现 Serializable 接口,不再需要 Cloneable
import java.io.Serializable;class Address implements Serializable { /* ... code ... */ }class Person implements Serializable { /* ... code ... */ }
测试序列化方式的深拷贝:
public class DeepCopySerializationDemo {public static void main(String[] args) {Address address = new Address("广州");Person person1 = new Person(25, address);// 使用工具类进行深拷贝Person person2 = DeepCopyUtil.deepCopy(person1);System.out.println("--- 修改前 ---");System.out.println("Person1: " + person1);System.out.println("Person2: " + person2);System.out.println("Person1.address == Person2.address ? " + (person1.getAddress() == person2.getAddress()));System.out.println("\n--- 修改副本 person2 ---");person2.setAge(26);person2.getAddress().city = "深圳";System.out.println("Person1: " + person1); // Person1 完全不受影响System.out.println("Person2: " + person2);}
}
优点:
- 代码简洁,一个工具类搞定所有。
- 不需要关心对象内部的复杂结构,只要所有成员都
Serializable
,就能正确复制。 - 非常健壮,是业界推荐的方式。
缺点:
- 性能开销比
clone()
大,因为涉及 I/O 操作。 - 所有相关的类都必须实现
Serializable
接口,有时可能无法做到(例如,第三方库的类)。
四、总结与对比
特性 | 浅拷贝 | 深拷贝 |
---|---|---|
基本类型 | 复制值,互不影响 | 复制值,互不影响 |
引用类型 | 复制引用,共享对象 | 创建新对象,独立 |
实现方式 | Object.clone() | 1. 手动重写 clone() 2. 序列化/反序列化 |
速度 | 快,只复制引用 | 慢,需要创建新对象 |
内存占用 | 较低,共享对象 | 较高,创建新对象 |
对象独立性 | 低,修改引用成员会影响原对象 | 高,完全独立 |
实现复杂度 | 简单 | 较复杂(手动clone )或简单(序列化) |
如何选择?
- 默认情况下,如果你没有特殊需求,Java 的
clone()
提供的就是浅拷贝。 使用时必须清楚其副作用。 - 当你的对象结构简单,且不包含引用类型,或者你明确希望共享内部引用时,可以使用浅拷贝。
- 当你需要一个与原对象完全独立的副本,以避免任何意外的副作用时,必须使用深拷贝。 在 Java 中,强烈推荐使用序列化/反序列化的方式来实现深拷贝,因为它更通用、更安全、更易于维护。