深入理解深拷贝与浅拷贝

在Java后端开发中,我们经常会遇到对象复制的需求。然而,简单地使用赋值操作符(=)往往无法满足我们的期望,尤其当对象中包含引用类型成员时。此时,深入理解“深拷贝”与“浅拷贝”的概念及其在Java中的实现方式变得至关重要。它们不仅影响着程序的行为,还可能引发难以察觉的Bug。

1. 什么是拷贝?

在Java中,当我们谈论“拷贝”时,实际上是在讨论如何创建一个现有对象的副本。为了更好地理解拷贝,我们首先需要回顾Java中数据在内存中的存储方式。

Java内存主要分为栈(Stack)和堆(Heap)。

  • :主要存储基本数据类型(如intbooleandouble等)的变量以及对象的引用(地址)。栈内存的分配和回收速度快,但容量有限。
  • :主要存储对象实例和数组。堆内存的分配和回收相对较慢,但容量更大,并且可以动态分配。

在Java中,变量可以分为两种类型:

  • 基本数据类型:直接存储值,如int a = 10;,变量a直接存储了整数值10。
  • 引用数据类型:存储的是对象的引用(内存地址),而不是对象本身。例如,Person p = new Person();,变量p存储的是Person对象在堆内存中的地址,通过这个地址才能访问到Person对象的实际数据。

理解了栈和堆以及基本数据类型和引用数据类型的区别,对于理解深拷贝和浅拷贝至关重要。拷贝的本质,就是如何处理这些数据在内存中的复制。

2. 浅拷贝(Shallow Copy)

2.1 定义

浅拷贝(Shallow Copy)是指在复制对象时,只复制对象本身及其包含的基本数据类型的值。对于对象中包含的引用类型成员,浅拷贝只会复制其引用地址,而不会复制引用地址指向的实际对象。这意味着,原对象和新对象会共享同一个引用类型成员所指向的内存空间。当其中一个对象修改了该引用类型成员的数据时,另一个对象也会受到影响,因为它们指向的是同一块内存区域。

简单来说,浅拷贝只复制了“壳”,而没有复制“内容”。如果“内容”是基本数据类型,那么“壳”和“内容”都复制了;如果“内容”是引用数据类型,那么“壳”复制了,但“内容”的引用地址复制了,实际的“内容”并没有被复制,而是被两个“壳”共享了。

2.2 实现方式

在Java中,实现浅拷贝最常见的方式是使用 Object 类提供的 clone() 方法。要使用 clone() 方法,需要满足以下条件:

  1. 实现 Cloneable 接口Cloneable 接口是一个标记接口,它不包含任何方法。它的作用是告诉JVM,实现这个接口的类可以被克隆。如果一个类没有实现 Cloneable 接口,而直接调用 clone() 方法,会抛出 CloneNotSupportedException 异常。
  2. 重写 clone() 方法Object 类的 clone() 方法是 protected 的,因此在子类中需要将其重写为 public,并调用 super.clone() 来完成浅拷贝。super.clone() 方法会创建一个新对象,并将原对象的所有字段(包括基本类型和引用类型)的值复制到新对象中。对于基本类型,是值的复制;对于引用类型,是引用的复制。

注意事项:

  • Object.clone() 方法执行的是浅拷贝。这意味着如果你的类中包含引用类型的成员变量,那么这些成员变量在拷贝后,原对象和新对象会共享同一个引用。对其中一个对象的引用类型成员的修改,会影响到另一个对象。
  • CloneNotSupportedException 是一个受检查异常,因此在使用 clone() 方法时需要进行异常处理。

2.3 示例代码

为了更好地理解浅拷贝的特性,我们来看一个示例。假设我们有一个 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 == student2false,说明 student1student2 是两个不同的对象,它们的内存地址不同。
  • student1.getCourse() == student2.getCourse()true,说明 student1student2course 成员指向的是同一个 Course 对象。当 student2 修改了 coursecourseName 时,student1courseName 也随之改变了。这正是浅拷贝的典型特征。
  • student2.setName("李四") 修改的是 student2 对象的 name 属性,由于 nameString 类型(不可变对象,可以视为基本类型),所以 student1name 属性不受影响。

3. 深拷贝(Deep Copy)

3.1 定义

深拷贝(Deep Copy)是指在复制对象时,不仅复制对象本身和其中包含的基本数据类型的值,还会递归地复制对象中所有引用类型成员所指向的实际对象。这意味着,原对象和新对象在内存中是完全独立的,它们拥有各自独立的引用类型成员副本。无论对原对象还是新对象的任何修改,都不会影响到另一个对象。

简单来说,深拷贝不仅复制了“壳”,还复制了“内容”,并且如果“内容”本身也是一个“壳”(即引用类型),那么这个“壳”里面的“内容”也会被递归地复制,直到所有层级的引用类型都被完全复制,从而确保新对象与原对象之间没有任何共享的引用。

3.2 实现方式

实现深拷贝有多种方式,下面介绍几种常用的方法。

方式一:重写 clone() 方法并递归调用

这是在Java中实现深拷贝最直接的方式,它基于浅拷贝的 clone() 方法,但需要对引用类型的成员进行额外的处理。对于每个引用类型的成员变量,都需要在其 clone() 方法中递归地调用该成员变量的 clone() 方法,以确保其指向的对象也被完全复制。

实现步骤:

  1. 被拷贝的类及其所有引用类型的成员变量的类都必须实现 Cloneable 接口。
  2. 在被拷贝的类中重写 clone() 方法,并在其中调用 super.clone() 获取浅拷贝。
  3. 对于每个引用类型的成员变量,手动调用其 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 == student2false,说明 student1student2 是两个不同的对象。
  • student1.getCourse() == student2.getCourse()false,说明 student1student2course 成员指向的是两个不同的 Course 对象。当 student2 修改了 coursecourseName 时,student1courseName 不受影响,依然是“高等数学”。这证明了深拷贝的独立性。
方式二:通过序列化与反序列化实现深拷贝

利用Java的序列化机制是实现深拷贝的一种非常方便且常用的方式。当一个对象被序列化时,它的整个对象图(包括它引用的所有对象)都会被写入到一个字节流中。然后,通过反序列化这个字节流,就可以重建一个完全独立的对象图,从而实现深拷贝。

实现步骤:

  1. 被拷贝的类及其所有引用类型的成员变量的类都必须实现 Serializable 接口。
  2. 使用 ObjectOutputStream 将对象写入到 ByteArrayOutputStream 中。
  3. 使用 ObjectInputStreamByteArrayInputStream 中读取对象。

优点:

  • 实现简单,不需要手动处理每个引用类型成员的拷贝。
  • 可以处理复杂的对象图,包括循环引用。

缺点:

  • 性能开销较大,因为涉及到I/O操作。
  • 所有相关的类都必须实现 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

使用第三方库可以减少样板代码,但其底层原理通常也是基于序列化或反射等方式。

4. 深拷贝与浅拷贝的选择

理解了深拷贝和浅拷贝的原理和实现方式后,关键在于如何在实际开发中做出正确的选择,这主要取决于你的业务需求和对对象独立性的要求。

4.1 场景分析

  • 何时使用浅拷贝?

    • 对象中只包含基本数据类型或不可变对象(如StringInteger等):在这种情况下,浅拷贝和深拷贝的效果是一致的,因为基本数据类型直接存储值,不可变对象一旦创建就不能修改,它们的“引用”实际上就是“值”。
    • 希望新旧对象共享部分引用类型成员:如果业务逻辑允许甚至要求新旧对象共享某些引用类型成员,例如,多个学生对象可以共享同一个课程对象(因为课程信息是固定的),那么浅拷贝是合适的选择。这可以节省内存空间,提高效率。
    • 性能要求极高,且共享引用不会导致问题:浅拷贝的性能通常优于深拷贝,因为它不需要递归地创建新对象。在对性能有极致要求的场景下,如果能够确保共享引用不会引发副作用,可以考虑使用浅拷贝。
  • 何时使用深拷贝?

    • 对象中包含引用类型成员,且需要完全独立的对象副本:这是使用深拷贝最主要的场景。当你希望对拷贝对象进行任何修改都不会影响到原对象时,就必须使用深拷贝。例如,在进行数据处理、算法模拟、状态快照等操作时,通常需要完全独立的数据副本。
    • 避免意外的副作用:浅拷贝可能导致难以追踪的Bug,因为对一个对象的修改可能会无意中影响到另一个对象。深拷贝可以有效避免这种“牵一发而动全身”的问题,提高代码的健壮性和可维护性。
    • 处理复杂对象图:当对象之间存在多层嵌套的引用关系时,浅拷贝无法满足需求。深拷贝能够递归地复制所有层级的对象,确保整个对象图的独立性。

4.2 性能考量

深拷贝通常比浅拷贝的性能开销更大,尤其是在对象图复杂或对象数量庞大的情况下。主要原因在于:

  • 内存分配:深拷贝需要为所有被复制的对象分配新的内存空间,这会增加内存分配和垃圾回收的负担。
  • CPU开销:无论是递归调用 clone() 方法还是通过序列化/反序列化,都涉及到更多的计算和I/O操作,从而消耗更多的CPU资源。

因此,在选择拷贝方式时,需要在对象独立性和性能之间进行权衡。如果性能是关键因素,并且能够接受部分共享引用,可以考虑浅拷贝。否则,为了保证数据的独立性和程序的正确性,深拷贝是更稳妥的选择。

4.3 实际开发中的应用

在实际的Java后端开发中,深拷贝和浅拷贝的应用场景非常广泛:

  • 缓存:当从缓存中获取一个对象时,如果直接返回原始对象,外部对该对象的修改会影响缓存中的数据。此时,通常会返回一个深拷贝的对象,以保证缓存数据的完整性。
  • 配置管理:应用程序的配置对象通常是单例的。如果需要修改配置,但又不希望影响到正在运行的应用程序,可以先对配置对象进行深拷贝,然后修改拷贝后的对象,最后再决定是否应用这些修改。
  • 设计模式:在某些设计模式中,如原型模式(Prototype Pattern),深拷贝是其核心实现。原型模式通过复制现有对象来创建新对象,从而避免了重复的初始化工作。
  • 事务处理:在数据库事务或分布式事务中,为了保证数据的一致性,可能需要对数据进行快照,此时深拷贝可以确保快照数据的独立性。
  • 数据传输:在微服务架构中,服务之间通过DTO(Data Transfer Object)进行数据传输。如果DTO中包含复杂的引用类型,为了避免数据污染,可能需要进行深拷贝。

5. 总结

深拷贝与浅拷贝是Java对象复制的两种基本方式,它们的核心区别在于对引用类型成员的处理:

  • 浅拷贝:只复制对象本身和基本数据类型,引用类型成员只复制引用地址,新旧对象共享引用类型成员指向的内存空间。
  • 深拷贝:复制对象本身、基本数据类型,并递归地复制所有引用类型成员所指向的实际对象,新旧对象完全独立。

在实际开发中,我们需要根据具体的业务需求和性能考量,选择合适的拷贝策略。当需要完全独立的对象副本时,深拷贝是必然的选择;而当对象结构简单或允许共享引用时,浅拷贝则更为高效。

你可能感兴趣的:(开发语言,java,后端)