Java中的深拷贝和浅拷贝
本文详解Java的浅拷贝与深拷贝。核心区别在于:浅拷贝共享内部引用,修改会互相影响;深拷贝则创建完全独立的副本,彼此隔离。文章还介绍了
clone()、构造函数和序列化等实现方式。
核心概念:什么是拷贝?
在Java中,除了基本数据类型(如 int, double, char 等)是按值传递的,对象都是按引用传递的。当你把一个对象变量赋值给另一个时,你只是复制了对象的引用(内存地址),而不是对象本身。
java
Person p1 = new Person("张三");
Person p2 = p1; // 这里没有创建新对象,p2和p1指向同一个内存地址
p2.setName("李四");
System.out.println(p1.getName()); // 输出 "李四"
而“拷贝”或“克隆”的目的是创建一个全新的、独立的对象,其内容与原对象相同。根据拷贝的“深度”,分为浅拷贝和深拷贝。
一个生动的比喻
想象一下,你有一份个人简历(Resume 对象),里面包含了你的基本信息(如姓名,是字符串)和一份工作经历列表(WorkExperience 对象)。
- 浅拷贝:就像你直接复印了这份简历。复印件上的姓名和你自己写的一样,但是工作经历部分,你只是复印了指向“同一份工作经历文档”的链接。如果你在原件上修改了这份工作经历文档(比如改了公司名),那么通过复印件上的链接找过去,看到的内容也是修改过的。
- 深拷贝:这相当于你不仅复印了简历的封面,还把简历里提到的那份“工作经历文档”也单独复印了一份,然后把这个新的工作经历文档的复印件附在新的简历复印件上。这样,你修改原件的工作经历,就完全不会影响到复印件里的工作经历。
浅拷贝 (Shallow Copy)
1. 定义
浅拷贝会创建一个新对象,然后将原始对象中的字段值精确地复制到新对象中。
- 对于基本数据类型(
int,double,boolean等):直接复制值。 - 对于引用类型(
String,Array, 自定义对象等):复制的是引用的地址,而不是引用所指向的对象本身。
2. 特点
- 新对象和原对象是两个独立的对象(内存地址不同)。
- 但它们内部的引用类型字段指向的是同一个对象。
- 因此,如果通过一个对象修改了其引用的对象内容,另一个对象也会受到影响。
3. 代码示例
我们使用 Object.clone() 方法来说明,这是Java中最常见的实现浅拷贝的方式。
java
// 地址类
class Address implements Cloneable {
private String city;
public Address(String city) { this.city = city; }
public void setCity(String city) { this.city = city; }
public String getCity() { return city; }
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
// 人员类
class Person implements Cloneable {
private int age;
private Address address; // 引用类型
public Person(int age, Address address) {
this.age = age;
this.address = address;
}
// 省略 getter/setter
public void setAge(int age) { this.age = age; }
public void setAddressCity(String city) { this.address.setCity(city); }
@Override
public String toString() {
return "Person [age=" + age + ", address=" + address.getCity() + "]";
}
// 实现浅拷贝
@Override
public Object clone() throws CloneNotSupportedException {
// super.clone() 默认是浅拷贝
return super.clone();
}
}
public class ShallowCopyTest {
public static void main(String[] args) throws CloneNotSupportedException {
Address address = new Address("北京");
Person p1 = new Person(30, address);
// 浅拷贝
Person p2 = (Person) p1.clone();
System.out.println("拷贝前:");
System.out.println("p1: " + p1); // p1: Person [age=30, address=北京]
System.out.println("p2: " + p2); // p2: Person [age=30, address=北京]
// 1. 修改基本数据类型
p2.setAge(35);
// 2. 修改引用类型的内容
p2.setAddressCity("上海");
System.out.println("\n拷贝后:");
System.out.println("p1: " + p1); // p1: Person [age=30, address=上海]
System.out.println("p2: " + p2); // p2: Person [age=35, address=上海]
// 结论:
// - 修改p2的基本类型age,p1的age未受影响。
// - 修改p2的引用类型address的内容,p1的address也跟着变了!因为它们共享同一个Address对象。
}
}
深拷贝 (Deep Copy)
1. 定义
深拷贝不仅会复制对象本身,还会递归地复制该对象内部引用的所有对象。
2. 特点
- 新对象和原对象是两个完全独立的对象。
- 它们内部的引用类型字段指向的也是全新的、独立的对象。
- 修改任何一个对象(包括其引用的对象),都不会影响到另一个。
3. 代码示例
我们修改上面 Person 类的 clone 方法来实现深拷贝。
java
class Person implements Cloneable {
private int age;
private Address address;
// ... 构造函数和 getter/setter 不变 ...
@Override
public String toString() {
return "Person [age=" + age + ", address=" + address.getCity() + "]";
}
// 实现深拷贝
@Override
public Object clone() throws CloneNotSupportedException {
// 1. 先对Person对象本身进行浅拷贝
Person clonedPerson = (Person) super.clone();
// 2. 对内部的引用类型字段,单独进行拷贝
clonedPerson.address = (Address) this.address.clone(); // 对Address对象也进行拷贝
return clonedPerson;
}
}
public class DeepCopyTest {
public static void main(String[] args) throws CloneNotSupportedException {
Address address = new Address("北京");
Person p1 = new Person(30, address);
// 深拷贝
Person p2 = (Person) p1.clone();
System.out.println("拷贝前:");
System.out.println("p1: " + p1); // p1: Person [age=30, address=北京]
System.out.println("p2: " + p2); // p2: Person [age=30, address=北京]
// 1. 修改基本数据类型
p2.setAge(35);
// 2. 修改引用类型的内容
p2.setAddressCity("上海");
System.out.println("\n拷贝后:");
System.out.println("p1: " + p1); // p1: Person [age=30, address=北京]
System.out.println("p2: " + p2); // p2: Person [age=35, address=上海]
// 结论:
// - 修改p2的age,p1不受影响。
// - 修改p2的address内容,p1的address也完全没有改变!因为p2的address是一个全新的对象。
}
}
实现拷贝的几种方式
实现
Cloneable接口并重写clone()方法 (如上例)- 优点: Java原生支持,对于复杂的对象图,性能可能较高。
- 缺点:
clone()方法是protected的,需要重写并声明为public。Cloneable是一个标记接口,没有方法,容易忘记实现。代码编写相对复杂,需要逐层处理引用对象的克隆。
使用拷贝构造函数 (Copy Constructor)
- 优点: 简单、直观、类型安全,可以自由控制拷贝逻辑。
- 缺点: 需要为每个类编写一个拷贝构造函数。
javaclass Person { private int age; private Address address; // 拷贝构造函数 public Person(Person original) { this.age = original.age; // 深拷贝 Address this.address = new Address(original.address.getCity()); } // ... 其他构造函数 ... } // 使用: Person p2 = new Person(p1);使用序列化 (Serialization)
- 将对象写入字节流,再从字节流中读回来,这个过程会创建一个全新的对象图。这是实现真正深拷贝的最简单、最彻底的方法。
- 优点: 实现简单,能处理复杂的对象图(包括循环引用)。
- 缺点: 性能开销较大。所有涉及的类都必须实现
java.io.Serializable接口。
javapublic static <T extends Serializable> T deepCopy(T obj) throws IOException, ClassNotFoundException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(obj); oos.close(); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); T copy = (T) ois.readObject(); ois.close(); return copy; } // 使用: Person p2 = deepCopy(p1);
总结对比
| 特性 | 浅拷贝 (Shallow Copy) | 深拷贝 (Deep Copy) |
|---|---|---|
| 定义 | 只复制对象本身和基本类型字段的值 | 递归复制对象及其引用的所有对象 |
| 引用类型字段 | 复制引用地址,新旧对象共享 | 创建并复制新的对象,新旧对象完全独立 |
| 独立性 | 不完全独立,修改引用对象会相互影响 | 完全独立,互不影响 |
| 实现复杂度 | 简单,Object.clone() 默认就是 |
相对复杂,需要手动处理每个引用类型 |
| 适用场景 | 对象内引用不可变(如String)或希望共享状态时 |
需要一个对象的完整独立快照,避免副作用 |
如何选择?
- 当你确定对象中的所有引用类型都是不可变(immutable)的(例如
String,Integer等),那么浅拷贝和深拷贝的效果是一样的,使用更简单的浅拷贝即可。 - 当你希望创建一个对象的副本,并且不希望副本的任何改动影响到原始对象时,必须使用深拷贝。
- 当性能是首要考虑因素,且可以接受共享某些内部对象时,浅拷贝是更好的选择。
右滑查看面试常问