这一篇足够让你理解深拷贝和浅拷贝(详细)

117 阅读11分钟

写在前面

如果觉得有所帮助,记得点个关注和点个赞哦,非常感谢支持。
任何变成语言中,其实都有浅拷贝和深拷贝的概念,Java 中也不例外。在对一个现有的对象进行拷贝操作的时候,是有浅拷贝和深拷贝之分的,他们在实际使用中,区别很大,如果对其进行混淆,可能会引发一些难以排查的问题。Java 中的数据类型分为基本数据类型和引用数据类型。对于这两种数据类型,在进行赋值操作、用作方法参数或返回值时,会有值传递和引用(地址)传递的差别。

什么是浅拷贝和深拷贝

首先需要明白,浅拷贝和深拷贝都是针对一个已有对象的操作。那先来看看浅拷贝和深拷贝的概念。上面讲了,在 Java 中,除了基本数据类型(元类型)之外,还存在类的实例对象这个引用数据类型。而一般使用 『 = 』号做赋值操作的时候。对于基本数据类型,实际上是拷贝的它的值,但是对于对象而言,其实赋值的只是这个对象的引用,将原对象的引用传递过去,他们实际上还是指向的同一个对象。

而浅拷贝和深拷贝就是在这个基础之上做的区分,如果在拷贝这个对象的时候,只对是基本数据类型的对象属性进行了拷贝,而对是引用数据类型的对象属性,只是进行了引用的传递,而没有真实的创建一个新的对象,则认为是浅拷贝。反之,在对是引用数据类型的对象属性进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量,则认为是深拷贝。这段话要仔细理解清楚,如果我们对象直接用“=”赋值,那么只是对象拷贝,不交浅拷贝和深拷贝。所以就应该了解了,所谓的浅拷贝和深拷贝,只是在拷贝对象的时候,针对对象所拥有的属性拷贝的说法,而且一定是使用了clone才算是浅拷贝和深拷贝(下面讲解代码的时候还会进行说明)。

  • 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。
    在这里插入图片描述
  • 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
    在这里插入图片描述

Java 中的拷贝

对象拷贝

  • 我们所知道的直接赋值,即使用“=”,对于基本数据类型而言,都是进行值传递;
  • 对于对象进行直接赋值,即使用“=”,都是引用传递

浅拷贝特点

  • 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个。
  • 对于引用类型的成员对象,比如数组或者类对象,因为引用类型是引用传递,所以浅拷贝只是把内存地址赋值给了成员变量,它们指向了同一内存空间。改变其中一个,会对另外一个也产生影响。

深拷贝特点

  • 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个(和浅拷贝一样)。
  • 对于引用类型的成员对象,比如数组或者类对象,深拷贝会新建一个对象空间,然后拷贝里面的内容,所以它们指向了不同的内存空间。改变其中一个,不会对另外一个也产生影响。
  • 对于有多层对象的成员对象,每个对象都需要实现 Cloneable 并重写 clone() 方法,进而实现了对象的串行层层拷贝。
  • 深拷贝相比于浅拷贝速度较慢并且花销较大。

通过代码实现进一步理解

上面的概念讲解还不理解的话,不要着急,这里我们使用代码来进行认识和思考,首先我们先要创建两个类,来为后面的讲解做准备,两个类分别是SubjectStudent,代码如下

public class Student {
    private Subject subject;
    private String name;
    private int age;

    public Subject getSubject() {
        return subject;
    }

    public void setSubject(Subject subject) {
        this.subject = subject;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
    

    @Override
    public String toString() {
        return "[Student: " + this.hashCode() + ",subject:" + subject + ",name:" + name + ",age:" + age + "]";
    }
}
public class Subject {

    private String name;

    public Subject(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "[Subject: " + this.hashCode() + ",name:" + name + "]";
    }
}

直接赋值

有了上面的代码之后呢,我们可以来体验一下直接赋值,我们知道,直接赋值对于基本数据类型而言,就是值传递,对于引用类型而言就是引用传递,比如,我们直接如下这样

public class Main {
    public static void main(String[] args) {
        Subject subjectA = new Subject("hahah");

        Student studentA = new Student();
        studentA.setAge(18);
        studentA.setName("qqqqq");
        studentA.setSubject(subjectA);

        //直接赋值,不使用clone进行深浅拷贝
        Student studentB = studentA;
        studentA.setAge(20);
        System.out.println(studentA);
        System.out.println(studentB);
    }
}

在这里插入图片描述
我们看到输出结果,发现哈希码都是一样的,说明 studentAstudentB 是指向了同一个对象,所以在更改age属性的时候,同时修改了。

浅拷贝

上面的操作叫做对象拷贝,那么我们接下来开始讲讲浅拷贝的代码,在讲解浅拷贝的代码之前,我们需要把Student类进行改造一下,让它实现Cloneable接口,并重写clone方法,Student改写之后代码如下

public class Student implements Cloneable {
    private Subject subject;
    private String name;
    private int age;

    public Subject getSubject() {
        return subject;
    }

    public void setSubject(Subject subject) {
        this.subject = subject;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        //浅拷贝,直接调用父类的clone()方法
        return super.clone();
    }

    @Override
    public String toString() {
        return "[Student: " + this.hashCode() + ",subject:" + subject + ",name:" + name + ",age:" + age + "]";
    }
}

这个时候,我们就可以调用Studentclone方法,来完成对象的浅拷贝,我们只需要在上面的Main方法中,讲直接赋值的对象拷贝,改成clone的拷贝赋值就可以了

public class Main {
    public static void main(String[] args) {
        Subject subjectA = new Subject("hahah");

        Student studentA = new Student();
        studentA.setAge(18);
        studentA.setName("qqqqq");
        studentA.setSubject(subjectA);

        //浅拷贝,使用clone进行浅拷贝
        Student studentB = null;
        try {
            studentB = (Student)studentA.clone();
        }catch (CloneNotSupportedException e){
            e.printStackTrace();
        }
        studentA.setAge(20);
        studentA.getSubject().setName("heihei");
        System.out.println(studentA);
        System.out.println(studentB);
    }
}

在这里插入图片描述
我们看到输出结果,发现哈希码不一样的,说明 studentAstudentB 不是指向了同一个对象,所以在studentA更改age属性的时候,只有studentA修改了。但是在修改studentA的subject的时候,发现了没有,两个同时修改了,studentA和studentB两个的subject属性的哈希码是一样的,所以这也就是浅拷贝,浅拷贝对于对象的基本类型属性,是值传递,而对于引用类型的属性,是引用传递。

深拷贝

上面的操作叫做浅拷贝,那么我们接下来开始讲讲深拷贝的代码,在讲解深拷贝的代码之前,我们需要接着把Student类的clone方法进行改造一下,不仅如此,我们还需要把Subject类改造,让它实现Cloneable接口,重写clone方法,代码如下

public class Subject implements Cloneable {

    private String name;

    public Subject(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return "[Subject: " + this.hashCode() + ",name:" + name + "]";
    }
}
public class Student implements Cloneable {
    //引用类型
    private Subject subject;
    //基础数据类型
    private String name;
    private int age;

    public Subject getSubject() {
        return subject;
    }

    public void setSubject(Subject subject) {
        this.subject = subject;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        //浅拷贝,直接调用父类的clone()方法
        Student student = (Student) super.clone();
        student.subject = (Subject) subject.clone();
        return student;
    }

    @Override
    public String toString() {
        return "[Student: " + this.hashCode() + ",subject:" + subject + ",name:" + name + ",age:" + age + "]";
    }
}

发现了没有,要想进行深拷贝,我们必须把拷贝对象里面的,所有引用类型属性的对象,都实现Cloneable接口,一层一层的属性对象,只要是引用类型的对象,都要实现Cloneable接口,这样才能实现深拷贝,我们接着使用上面浅拷贝的Main方法来检测一下深拷贝的结果

public class Main {
    public static void main(String[] args) {
        Subject subjectA = new Subject("hahah");

        Student studentA = new Student();
        studentA.setAge(18);
        studentA.setName("qqqqq");
        studentA.setSubject(subjectA);

        //深拷贝,使用clone进行深拷贝
        Student studentB = null;
        try {
            studentB = (Student)studentA.clone();
        }catch (CloneNotSupportedException e){
            e.printStackTrace();
        }
        studentA.setAge(20);
        studentA.getSubject().setName("heihei");
        System.out.println(studentA);
        System.out.println(studentB);
    }
}

在这里插入图片描述
由输出结果可见,深拷贝后,不管是基础数据类型还是引用类型的成员变量,修改其值都不会相互造成影响。

通过序列化实现深拷贝

也可以通过序列化来实现深拷贝。序列化是干什么的?它将整个对象图写入到一个持久化存储文件中并且当需要的时候把它读取回来, 这意味着当你需要把它读取回来时你需要整个对象图的一个拷贝。这就是当你深拷贝一个对象时真正需要的东西。请注意,当你通过序列化进行深拷贝时,必须确保对象图中所有类都是可序列化的。首先我们先对StudentSubject进行改造,这两个类都要实现Serializable接口,代码如下:

import java.io.Serializable;

public class Student implements Serializable {
    //引用类型
    private Subject subject;
    //基础数据类型
    private String name;
    private int age;

    public Subject getSubject() {
        return subject;
    }

    public void setSubject(Subject subject) {
        this.subject = subject;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "[Student: " + this.hashCode() + ",subject:" + subject + ",name:" + name + ",age:" + age + "]";
    }
}
import java.io.Serializable;

public class Subject implements Serializable {

    private String name;

    public Subject(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "[Subject: " + this.hashCode() + ",name:" + name + "]";
    }
}

我们完成了这两个类的改造之后呢,我们接下来在Main方法中测试他们,Main方法的内容如下:

public class Main {
    public static void main(String[] args) throws Exception {
        Subject subjectA = new Subject("hahah");

        Student studentA = new Student();
        studentA.setAge(18);
        studentA.setName("qqqqq");
        studentA.setSubject(subjectA);

        // 通过序列化实现深拷贝
        ObjectOutputStream objectOutputStream = null;
        ObjectInputStream objectInputStream = null;
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        objectOutputStream = new ObjectOutputStream(outputStream);
        // 序列化以及传递这个对象
        objectOutputStream.writeObject(studentA);
        objectOutputStream.flush();
        ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
        objectInputStream = new ObjectInputStream(inputStream);
        // 返回新的对象
        Student studentB = (Student)objectInputStream.readObject();

        studentB.setAge(20);
        studentB.setName("wwwww");
        studentB.getSubject().setName("heihei");
        System.out.println(studentA);
        System.out.println(studentB);
    }
}

在这里插入图片描述

延迟拷贝

延迟拷贝是浅拷贝和深拷贝的一个组合,实际上很少会使用。 当最开始拷贝一个对象时,会使用速度较快的浅拷贝,还会使用一个计数器来记录有多少对象共享这个数据。当程序想要修改原始的对象时,它会决定数据是否被共享(通过检查计数器)并根据需要进行深拷贝。延迟拷贝从外面看起来就是深拷贝,但是只要有可能它就会利用浅拷贝的速度。当原始对象中的引用不经常改变的时候可以使用延迟拷贝。由于存在计数器,效率下降很高,但只是常量级的开销。而且,在某些情况下,循环引用会导致一些问题。

总结

如果对象的属性全是基本类型的,那么可以使用浅拷贝,但是如果对象有引用属性,那就要基于具体的需求来选择浅拷贝还是深拷贝。我的意思是如果对象引用任何时候都不会被改变,那么没必要使用深拷贝,只需要使用浅拷贝就行了。如果对象引用经常改变,那么就要使用深拷贝。没有一成不变的规则,一切都取决于具体需求。