Java 中什么是浅拷贝?什么是深拷贝?
一、核心理解
在 Java 中,浅拷贝 和深拷贝都是对象复制方式。
它们真正的区别在于:
对象内部如果包含引用类型成员变量,复制时到底是复制"引用地址",还是复制"引用对象本身"。
简单记忆:
浅拷贝:只复制外层对象,内部引用对象仍然共用。
深拷贝:外层对象复制,内部引用对象也重新复制一份。
二、先准备两个类
为了方便理解,先定义两个类:
java
class Address {
String city;
public Address(String city) {
this.city = city;
}
}
class Student {
String name;
Address address;
public Student(String name, Address address) {
this.name = name;
this.address = address;
}
}
Student 类中有两个字段:
java
String name;
Address address;
其中:
name可以简单理解为普通数据;address是一个对象引用,指向一个Address对象。
这类"对象中包含另一个对象"的情况,就是理解浅拷贝和深拷贝的关键。
三、什么是浅拷贝?
1. 浅拷贝的含义
浅拷贝指的是:
创建一个新的外层对象,但是对象内部的引用类型字段,仍然指向原来的对象。
也就是说:
java
Student s1 = new Student("Ken", new Address("西安"));
Student s2 = s1 的浅拷贝;
拷贝之后:
text
s1 和 s2 是两个不同的 Student 对象
但是 s1.address 和 s2.address 指向同一个 Address 对象
2. 浅拷贝代码示例
Java 中可以通过 Object 类的 clone() 方法实现浅拷贝。
java
class Address {
String city;
public Address(String city) {
this.city = city;
}
}
class Student implements Cloneable {
String name;
Address address;
public Student(String name, Address address) {
this.name = name;
this.address = address;
}
@Override
protected Student clone() throws CloneNotSupportedException {
return (Student) super.clone();
}
}
测试代码:
java
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Address address = new Address("西安");
Student s1 = new Student("Ken", address);
Student s2 = s1.clone();
s2.name = "Tom";
s2.address.city = "北京";
System.out.println(s1.name); // Ken
System.out.println(s1.address.city); // 北京
}
}
3. 为什么 s1.address.city 也变了?
因为浅拷贝之后:
java
s1 != s2
说明 s1 和 s2 是两个不同的 Student 对象。
但是:
java
s1.address == s2.address
说明它们内部的 address 指向同一个 Address 对象。
所以当执行:
java
s2.address.city = "北京";
实际上修改的是两个学生对象共同引用的那个地址对象。
因此:
java
System.out.println(s1.address.city);
输出结果也会变成:
text
北京
4. 浅拷贝图示
text
s1 ---> Student对象1
name = "Ken"
address ----+
|
s2 ---> Student对象2 |
name = "Ken" |
address ----+
|
Address对象
city = "西安"
浅拷贝的重点是:
外层对象变成两个了,但是里面引用的对象仍然是同一个。
四、什么是深拷贝?
1. 深拷贝的含义
深拷贝指的是:
创建一个新的外层对象,并且把对象内部引用的对象也重新复制一份。
也就是说:
text
s1 和 s2 是两个不同的 Student 对象
s1.address 和 s2.address 也是两个不同的 Address 对象
这样修改 s2.address.city,就不会影响 s1.address.city。
2. 深拷贝代码示例
首先让 Address 也支持克隆:
java
class Address implements Cloneable {
String city;
public Address(String city) {
this.city = city;
}
@Override
protected Address clone() throws CloneNotSupportedException {
return (Address) super.clone();
}
}
然后在 Student 的 clone() 方法中,手动复制 address:
java
class Student implements Cloneable {
String name;
Address address;
public Student(String name, Address address) {
this.name = name;
this.address = address;
}
@Override
protected Student clone() throws CloneNotSupportedException {
Student student = (Student) super.clone();
// 关键:引用类型字段也要单独复制
student.address = this.address.clone();
return student;
}
}
测试代码:
java
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Address address = new Address("西安");
Student s1 = new Student("Ken", address);
Student s2 = s1.clone();
s2.name = "Tom";
s2.address.city = "北京";
System.out.println(s1.name); // Ken
System.out.println(s1.address.city); // 西安
System.out.println(s2.name); // Tom
System.out.println(s2.address.city); // 北京
}
}
3. 为什么深拷贝不会互相影响?
因为深拷贝之后:
java
s1 != s2
说明两个 Student 对象不同。
同时:
java
s1.address != s2.address
说明两个 Address 对象也不同。
所以修改:
java
s2.address.city = "北京";
只会影响 s2 自己的地址对象,不会影响 s1 的地址对象。
4. 深拷贝图示
text
s1 ---> Student对象1
name = "Ken"
address ---> Address对象1
city = "西安"
s2 ---> Student对象2
name = "Ken"
address ---> Address对象2
city = "西安"
深拷贝的重点是:
外层对象复制了,里面引用的对象也复制了。
五、浅拷贝和深拷贝对比
| 对比点 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 是否创建新的外层对象 | 是 | 是 |
| 基本类型字段 | 复制值 | 复制值 |
| 引用类型字段 | 复制引用地址 | 复制引用对象本身 |
| 内部对象是否共享 | 共享 | 不共享 |
| 修改内部对象是否互相影响 | 会影响 | 不会影响 |
| 实现难度 | 简单 | 较复杂 |
| 常见实现方式 | super.clone() |
手动复制引用对象、序列化、拷贝构造方法 |
六、再用生活例子理解
1. 浅拷贝
浅拷贝就像:
复印了一份学生档案,但是家庭住址卡片还是共用同一张。
所以如果有人修改了这张家庭住址卡片,两份档案看到的地址都会变化。
2. 深拷贝
深拷贝就像:
复印了一份学生档案,并且连家庭住址卡片也重新复印了一份。
所以修改其中一份档案里的地址卡片,不会影响另一份档案。
七、常见实现方式
1. 使用 clone() 方法
浅拷贝:
java
@Override
protected Student clone() throws CloneNotSupportedException {
return (Student) super.clone();
}
深拷贝:
java
@Override
protected Student clone() throws CloneNotSupportedException {
Student student = (Student) super.clone();
student.address = this.address.clone();
return student;
}
2. 使用拷贝构造方法
相比 clone(),拷贝构造方法更直观。
java
class Address {
String city;
public Address(String city) {
this.city = city;
}
public Address(Address other) {
this.city = other.city;
}
}
class Student {
String name;
Address address;
public Student(String name, Address address) {
this.name = name;
this.address = address;
}
public Student(Student other) {
this.name = other.name;
this.address = new Address(other.address);
}
}
测试:
java
public class Main {
public static void main(String[] args) {
Student s1 = new Student("Ken", new Address("西安"));
Student s2 = new Student(s1);
s2.address.city = "北京";
System.out.println(s1.address.city); // 西安
System.out.println(s2.address.city); // 北京
}
}
这种方式本质上也是深拷贝。
3. 使用序列化实现深拷贝
如果对象层级比较复杂,也可以通过序列化和反序列化实现深拷贝。
不过这种方式性能开销较大,实际开发中要谨慎使用。
示意代码:
java
// 将对象写入字节流,再从字节流中读出来
// 读出来的新对象与原对象完全独立
八、容易踩坑的地方
1. String 是否需要深拷贝?
一般情况下不需要特别处理 String。
因为 String 是不可变对象:
java
String name = "Ken";
当你修改字符串时,并不是修改原来的字符串对象,而是创建新的字符串对象。
所以在很多示例中,我们重点关注的是自定义引用对象,比如:
java
Address address;
2. 只有一层深拷贝够不够?
不一定。
如果对象结构是这样的:
text
Student
└── Address
└── CityInfo
那么深拷贝时,不仅要复制 Address,还要继续复制 CityInfo。
否则仍然可能出现内部对象共享的问题。
所以深拷贝本质上是:
对象引用链上的所有可变引用对象,都要复制一份。
3. clone() 默认就是深拷贝吗?
不是。
Object 类的 clone() 方法默认是浅拷贝。
也就是说:
java
super.clone();
只会复制当前对象本身,不会自动复制它内部引用的其他对象。
如果想实现深拷贝,需要你自己手动处理引用类型字段。
九、面试回答模板
可以这样回答:
Java 中浅拷贝和深拷贝都是对象复制方式。浅拷贝会创建一个新的外层对象,但是对象内部的引用类型字段仍然指向原来的对象,因此两个对象会共享内部引用对象。深拷贝不仅会复制外层对象,还会复制对象内部引用的对象,因此两个对象之间是相互独立的。Java 中
Object类的clone()方法默认实现的是浅拷贝,如果要实现深拷贝,需要对引用类型字段手动再次复制,或者使用拷贝构造方法、序列化等方式实现。
十、最终总结
一句话总结:
浅拷贝复制对象本身,深拷贝复制对象本身以及它引用的对象。
再简单一点:
浅拷贝:外壳复制,里面共用。
深拷贝:外壳复制,里面也复制。
判断一个拷贝是浅拷贝还是深拷贝,关键看:
java
原对象.引用字段 == 拷贝对象.引用字段
如果结果是 true,通常就是浅拷贝。
如果结果是 false,说明引用对象也被复制了,更接近深拷贝。