在Java后端开发中,我们经常会遇到对象复制的需求。然而,简单地使用赋值操作符(
=
)往往无法满足我们的期望,尤其当对象中包含引用类型成员时。此时,深入理解“深拷贝”与“浅拷贝”的概念及其在Java中的实现方式变得至关重要。它们不仅影响着程序的行为,还可能引发难以察觉的Bug。
在Java中,当我们谈论“拷贝”时,实际上是在讨论如何创建一个现有对象的副本。为了更好地理解拷贝,我们首先需要回顾Java中数据在内存中的存储方式。
Java内存主要分为栈(Stack)和堆(Heap)。
int
、boolean
、double
等)的变量以及对象的引用(地址)。栈内存的分配和回收速度快,但容量有限。在Java中,变量可以分为两种类型:
int a = 10;
,变量a
直接存储了整数值10。Person p = new Person();
,变量p
存储的是Person
对象在堆内存中的地址,通过这个地址才能访问到Person
对象的实际数据。理解了栈和堆以及基本数据类型和引用数据类型的区别,对于理解深拷贝和浅拷贝至关重要。拷贝的本质,就是如何处理这些数据在内存中的复制。
浅拷贝(Shallow Copy)是指在复制对象时,只复制对象本身及其包含的基本数据类型的值。对于对象中包含的引用类型成员,浅拷贝只会复制其引用地址,而不会复制引用地址指向的实际对象。这意味着,原对象和新对象会共享同一个引用类型成员所指向的内存空间。当其中一个对象修改了该引用类型成员的数据时,另一个对象也会受到影响,因为它们指向的是同一块内存区域。
简单来说,浅拷贝只复制了“壳”,而没有复制“内容”。如果“内容”是基本数据类型,那么“壳”和“内容”都复制了;如果“内容”是引用数据类型,那么“壳”复制了,但“内容”的引用地址复制了,实际的“内容”并没有被复制,而是被两个“壳”共享了。
在Java中,实现浅拷贝最常见的方式是使用 Object
类提供的 clone()
方法。要使用 clone()
方法,需要满足以下条件:
Cloneable
接口:Cloneable
接口是一个标记接口,它不包含任何方法。它的作用是告诉JVM,实现这个接口的类可以被克隆。如果一个类没有实现 Cloneable
接口,而直接调用 clone()
方法,会抛出 CloneNotSupportedException
异常。clone()
方法:Object
类的 clone()
方法是 protected
的,因此在子类中需要将其重写为 public
,并调用 super.clone()
来完成浅拷贝。super.clone()
方法会创建一个新对象,并将原对象的所有字段(包括基本类型和引用类型)的值复制到新对象中。对于基本类型,是值的复制;对于引用类型,是引用的复制。注意事项:
Object.clone()
方法执行的是浅拷贝。这意味着如果你的类中包含引用类型的成员变量,那么这些成员变量在拷贝后,原对象和新对象会共享同一个引用。对其中一个对象的引用类型成员的修改,会影响到另一个对象。CloneNotSupportedException
是一个受检查异常,因此在使用 clone()
方法时需要进行异常处理。为了更好地理解浅拷贝的特性,我们来看一个示例。假设我们有一个 Student
类,其中包含一个 String
类型的 name
和一个 Course
类型的 course
。
import java.util.Objects;
class Course {
private String courseName;
public Course(String courseName) {
this.courseName = courseName;
}
public String getCourseName() {
return courseName;
}
public void setCourseName(String courseName) {
this.courseName = courseName;
}
@Override
public String toString() {
return "Course{courseName='" + courseName + "'}";
}
}
class Student implements Cloneable {
private String name;
private Course course;
public Student(String name, Course course) {
this.name = name;
this.course = course;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Course getCourse() {
return course;
}
public void setCourse(Course course) {
this.course = course;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "Student{name='" + name + "', course=" + course + "}";
}
}
public class ShallowCopyExample {
public static void main(String[] args) {
Course mathCourse = new Course("高等数学");
Student student1 = new Student("张三", mathCourse);
try {
Student student2 = (Student) student1.clone();
System.out.println("原始对象 (student1): " + student1);
System.out.println("拷贝对象 (student2): " + student2);
System.out.println("\n--- 修改拷贝对象后 ---");
// 修改拷贝对象的引用类型成员
student2.getCourse().setCourseName("线性代数");
student2.setName("李四");
System.out.println("原始对象 (student1): " + student1);
System.out.println("拷贝对象 (student2): " + student2);
System.out.println("\n--- 内存地址比较 ---");
System.out.println("student1 == student2: " + (student1 == student2));
System.out.println("student1.getCourse() == student2.getCourse(): " + (student1.getCourse() == student2.getCourse()));
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
运行结果:
原始对象 (student1): Student{name='张三', course=Course{courseName='高等数学'}}
拷贝对象 (student2): Student{name='张三', course=Course{courseName='高等数学'}}
--- 修改拷贝对象后 ---
原始对象 (student1): Student{name='张三', course=Course{courseName='线性代数'}}
拷贝对象 (student2): Student{name='李四', course=Course{courseName='线性代数'}}
--- 内存地址比较 ---
student1 == student2: false
student1.getCourse() == student2.getCourse(): true
从运行结果可以看出:
student1 == student2
为 false
,说明 student1
和 student2
是两个不同的对象,它们的内存地址不同。student1.getCourse() == student2.getCourse()
为 true
,说明 student1
和 student2
的 course
成员指向的是同一个 Course
对象。当 student2
修改了 course
的 courseName
时,student1
的 courseName
也随之改变了。这正是浅拷贝的典型特征。student2.setName("李四")
修改的是 student2
对象的 name
属性,由于 name
是 String
类型(不可变对象,可以视为基本类型),所以 student1
的 name
属性不受影响。深拷贝(Deep Copy)是指在复制对象时,不仅复制对象本身和其中包含的基本数据类型的值,还会递归地复制对象中所有引用类型成员所指向的实际对象。这意味着,原对象和新对象在内存中是完全独立的,它们拥有各自独立的引用类型成员副本。无论对原对象还是新对象的任何修改,都不会影响到另一个对象。
简单来说,深拷贝不仅复制了“壳”,还复制了“内容”,并且如果“内容”本身也是一个“壳”(即引用类型),那么这个“壳”里面的“内容”也会被递归地复制,直到所有层级的引用类型都被完全复制,从而确保新对象与原对象之间没有任何共享的引用。
实现深拷贝有多种方式,下面介绍几种常用的方法。
clone()
方法并递归调用这是在Java中实现深拷贝最直接的方式,它基于浅拷贝的 clone()
方法,但需要对引用类型的成员进行额外的处理。对于每个引用类型的成员变量,都需要在其 clone()
方法中递归地调用该成员变量的 clone()
方法,以确保其指向的对象也被完全复制。
实现步骤:
Cloneable
接口。clone()
方法,并在其中调用 super.clone()
获取浅拷贝。clone()
方法,并将返回的新对象赋值给新对象的对应成员变量。示例代码:
我们修改 Student
类和 Course
类,使其支持深拷贝。
import java.util.Objects;
class Course implements Cloneable {
private String courseName;
public Course(String courseName) {
this.courseName = courseName;
}
public String getCourseName() {
return courseName;
}
public void setCourseName(String courseName) {
this.courseName = courseName;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "Course{courseName=\'" + courseName + "\'}";
}
}
class Student implements Cloneable {
private String name;
private Course course;
public Student(String name, Course course) {
this.name = name;
this.course = course;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Course getCourse() {
return course;
}
public void setCourse(Course course) {
this.course = course;
}
@Override
protected Object clone() throws CloneNotSupportedException {
Student clonedStudent = (Student) super.clone(); // 浅拷贝
clonedStudent.course = (Course) course.clone(); // 对引用类型进行深拷贝
return clonedStudent;
}
@Override
public String toString() {
return "Student{name=\'" + name + "\', course=" + course + "}";
}
}
public class DeepCopyByCloneExample {
public static void main(String[] args) {
Course mathCourse = new Course("高等数学");
Student student1 = new Student("张三", mathCourse);
try {
Student student2 = (Student) student1.clone();
System.out.println("原始对象 (student1): " + student1);
System.out.println("拷贝对象 (student2): " + student2);
System.out.println("\n--- 修改拷贝对象后 ---");
// 修改拷贝对象的引用类型成员
student2.getCourse().setCourseName("线性代数");
student2.setName("李四");
System.out.println("原始对象 (student1): " + student1);
System.out.println("拷贝对象 (student2): " + student2);
System.out.println("\n--- 内存地址比较 ---");
System.out.println("student1 == student2: " + (student1 == student2));
System.out.println("student1.getCourse() == student2.getCourse(): " + (student1.getCourse() == student2.getCourse()));
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
运行结果:
原始对象 (student1): Student{name=\'张三\', course=Course{courseName=\'高等数学\'}}
拷贝对象 (student2): Student{name=\'张三\', course=Course{courseName=\'高等数学\'}}
--- 修改拷贝对象后 ---
原始对象 (student1): Student{name=\'张三\', course=Course{courseName=\'高等数学\'}}
拷贝对象 (student2): Student{name=\'李四\', course=Course{courseName=\'线性代数\'}}
--- 内存地址比较 ---
student1 == student2: false
student1.getCourse() == student2.getCourse(): false
从运行结果可以看出:
student1 == student2
为 false
,说明 student1
和 student2
是两个不同的对象。student1.getCourse() == student2.getCourse()
为 false
,说明 student1
和 student2
的 course
成员指向的是两个不同的 Course
对象。当 student2
修改了 course
的 courseName
时,student1
的 courseName
不受影响,依然是“高等数学”。这证明了深拷贝的独立性。利用Java的序列化机制是实现深拷贝的一种非常方便且常用的方式。当一个对象被序列化时,它的整个对象图(包括它引用的所有对象)都会被写入到一个字节流中。然后,通过反序列化这个字节流,就可以重建一个完全独立的对象图,从而实现深拷贝。
实现步骤:
Serializable
接口。ObjectOutputStream
将对象写入到 ByteArrayOutputStream
中。ObjectInputStream
从 ByteArrayInputStream
中读取对象。优点:
缺点:
Serializable
接口,这可能会对类的设计产生侵入性。transient
关键字修饰的字段不会被序列化,因此不会被深拷贝。示例代码:
import java.io.*;
class Course implements Serializable {
private static final long serialVersionUID = 1L;
private String courseName;
public Course(String courseName) {
this.courseName = courseName;
}
public String getCourseName() {
return courseName;
}
public void setCourseName(String courseName) {
this.courseName = courseName;
}
@Override
public String toString() {
return "Course{courseName=\'" + courseName + "\'}";
}
}
class Student implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private Course course;
public Student(String name, Course course) {
this.name = name;
this.course = course;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Course getCourse() {
return course;
}
public void setCourse(Course course) {
this.course = course;
}
@Override
public String toString() {
return "Student{name=\'" + name + "\', course=" + course + "}";
}
}
public class DeepCopyBySerializationExample {
public static void main(String[] args) {
Course mathCourse = new Course("高等数学");
Student student1 = new Student("张三", mathCourse);
try {
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(student1);
// 反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
Student student2 = (Student) ois.readObject();
System.out.println("原始对象 (student1): " + student1);
System.out.println("拷贝对象 (student2): " + student2);
System.out.println("\n--- 修改拷贝对象后 ---");
// 修改拷贝对象的引用类型成员
student2.getCourse().setCourseName("线性代数");
student2.setName("李四");
System.out.println("原始对象 (student1): " + student1);
System.out.println("拷贝对象 (student2): " + student2);
System.out.println("\n--- 内存地址比较 ---");
System.out.println("student1 == student2: " + (student1 == student2));
System.out.println("student1.getCourse() == student2.getCourse(): " + (student1.getCourse() == student2.getCourse()));
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
运行结果:
原始对象 (student1): Student{name=\'张三\', course=Course{courseName=\'高等数学\'}}
拷贝对象 (student2): Student{name=\'张三\', course=Course{courseName=\'高等数学\'}}
--- 修改拷贝对象后 ---
原始对象 (student1): Student{name=\'张三\', course=Course{courseName=\'高等数学\'}}
拷贝对象 (student2): Student{name=\'李四\', course=Course{courseName=\'线性代数\'}}
--- 内存地址比较 ---
student1 == student2: false
student1.getCourse() == student2.getCourse(): false
从运行结果可以看出,通过序列化和反序列化实现的深拷贝,同样保证了原对象和拷贝对象之间的完全独立性。
在实际开发中,为了简化深拷贝的实现,也可以考虑使用一些成熟的第三方库。例如,Apache Commons Lang 库中的 SerializationUtils
工具类提供了 clone()
方法,可以方便地实现基于序列化的深拷贝。当然,这同样要求被拷贝的类实现 Serializable
接口。
// 引入Apache Commons Lang库
//
// org.apache.commons
// commons-lang3
// 3.12.0
//
import org.apache.commons.lang3.SerializationUtils;
// ... Course 和 Student 类同上,需要实现Serializable接口 ...
public class DeepCopyByApacheCommonsExample {
public static void main(String[] args) {
Course mathCourse = new Course("高等数学");
Student student1 = new Student("张三", mathCourse);
Student student2 = (Student) SerializationUtils.clone(student1);
System.out.println("原始对象 (student1): " + student1);
System.out.println("拷贝对象 (student2): " + student2);
System.out.println("\n--- 修改拷贝对象后 ---");
// 修改拷贝对象的引用类型成员
student2.getCourse().setCourseName("线性代数");
student2.setName("李四");
System.out.println("原始对象 (student1): " + student1);
System.out.println("拷贝对象 (student2): " + student2);
System.out.println("\n--- 内存地址比较 ---");
System.out.println("student1 == student2: " + (student1 == student2));
System.out.println("student1.getCourse() == student2.getCourse(): " + (student1.getCourse() == student2.getCourse()));
}
}
运行结果:
原始对象 (student1): Student{name=\'张三\', course=Course{courseName=\'高等数学\'}}
拷贝对象 (student2): Student{name=\'张三\', course=Course{courseName=\'高等数学\'}}
--- 修改拷贝对象后 ---
原始对象 (student1): Student{name=\'张三\', course=Course{courseName=\'高等数学\'}}
拷贝对象 (student2): Student{name=\'李四\', course=Course{courseName=\'线性代数\'}}
--- 内存地址比较 ---
student1 == student2: false
student1.getCourse() == student2.getCourse(): false
使用第三方库可以减少样板代码,但其底层原理通常也是基于序列化或反射等方式。
理解了深拷贝和浅拷贝的原理和实现方式后,关键在于如何在实际开发中做出正确的选择,这主要取决于你的业务需求和对对象独立性的要求。
何时使用浅拷贝?
String
、Integer
等):在这种情况下,浅拷贝和深拷贝的效果是一致的,因为基本数据类型直接存储值,不可变对象一旦创建就不能修改,它们的“引用”实际上就是“值”。何时使用深拷贝?
深拷贝通常比浅拷贝的性能开销更大,尤其是在对象图复杂或对象数量庞大的情况下。主要原因在于:
clone()
方法还是通过序列化/反序列化,都涉及到更多的计算和I/O操作,从而消耗更多的CPU资源。因此,在选择拷贝方式时,需要在对象独立性和性能之间进行权衡。如果性能是关键因素,并且能够接受部分共享引用,可以考虑浅拷贝。否则,为了保证数据的独立性和程序的正确性,深拷贝是更稳妥的选择。
在实际的Java后端开发中,深拷贝和浅拷贝的应用场景非常广泛:
深拷贝与浅拷贝是Java对象复制的两种基本方式,它们的核心区别在于对引用类型成员的处理:
在实际开发中,我们需要根据具体的业务需求和性能考量,选择合适的拷贝策略。当需要完全独立的对象副本时,深拷贝是必然的选择;而当对象结构简单或允许共享引用时,浅拷贝则更为高效。