Python中的浅拷贝和深拷贝中我们探究了Python中关于浅拷贝和深拷贝的一些内容,对于不同类型数据的拷贝原理有了初步的了解。下面我们继续看一下同样是面向对象语言的Java中浅拷贝和深拷贝有什么联系和区别,并且从更深层次上进行理解。本文将通过图示的方法来剖析Java中对象拷贝(克隆)的相关内容。希望在阅读之后可以对这块内容有个较为清晰的认识,同时更好的理解JVM关于内存空间的划分。

1. 引入

Java中的浅拷贝和深拷贝都可以统称为对象克隆(clone),它指的是将一个对象的所有属性(成员变量)克隆到另一个有着相同类类型的对象中。如果现在将某一类型的数据进行一次拷贝,最直接的方法是什么呢?首先想到的仍然是=
基本数据类型数据copy.png

  • 如果待拷贝数据是基本数据类型变量:由于它们都是保存在栈内存中,因此,拷贝操作会为新变量直接在栈内存中开辟一块空间,并将待拷贝变量的值保存到开辟的空间中,此时原变量和副本本身就是两个不同的变量,它们指向的内存地址空间是不同的
  • 如果待拷贝数据是基本类型的数组:回想我们新建数组是怎么写代码的,如果想新建一个int型数组,通常可写作
    1. int[] arr = new int[]{1, 2, 3};
    2. // or
    3. int[] arr = {1, 2, 3};


从中可以看出,数组的创建需要使用new关键字。因此,数组实际保存在堆内存中,数组变量在栈内存中,它所保存的内容实际上就是堆内存地址。如果使用= 直接进行拷贝,那么此时传递的就是数组对象的引用,即堆内存地址。因此,原变量和副本指向的是同一个数组,其中一个对于数组元素的改变必然会影响另一个。

2. 浅拷贝

浅拷贝是Java中对象克隆的一种,如果想要实现对象的拷贝,类需要实现Cloneable接口,实现接口中的clone() ,并且方法的修饰符为public。在了解了基本的实现原理后,我们首先先看一下对于类对象使用= 进行拷贝有什么现象发生。

假设此时定义一个Student类,我们并不实现Cloneable接口和其中的clone() ,定义如下:

  1. public class Student{
  2. private String name;
  3. private int age;
  4. public Student() {
  5. }
  6. public Student(String name, int age) {
  7. this.name = name;
  8. this.age = age;
  9. }
  10. public String getName() {
  11. return name;
  12. }
  13. public void setName(String name) {
  14. this.name = name;
  15. }
  16. public int getAge() {
  17. return age;
  18. }
  19. public void setAge(int age) {
  20. this.age = age;
  21. }
  22. }

如果创建两个属性值不同的类对象,它们会相等嘛?答案显然是NO!,因为它们首先属性值不同,另一个方面使用两次new创建的对象在堆内存中拥有不同的地址,所以它们是不同的两个对象。

  1. public class CloneDemo {
  2. public static void main(String[] args) {
  3. Student s1 = new Student("Forlogen", 10);
  4. Student s2 = new Student("Kobe", 24);
  5. System.out.println("s1 is: " + s1 + " and s2 is: " + s2);
  6. // s1 is: Clone.Student@135fbaa4 and s2 is: Clone.Student@45ee12a7
  7. System.out.println(s1 == s2); // fasle
  8. }
  9. }

如果创建两个属性值相同的类对象呢?它们依然是两个不同的对象!因为,不管属性如何赋值,归根结底仍然是通过new来分别实例化的对象,因此它们的地址不同,自然对象也就不相同喽。

  1. public class CloneDemo {
  2. public static void main(String[] args) {
  3. Student s3 = new Student("Forlogen", 10);
  4. Student s4 = new Student("Forlogen", 10);
  5. System.out.println("s3 is: " + s3 + " and s4 is: " + s4);
  6. // s3 is: Clone.Student@330bedb4 and s4 is: Clone.Student@2503dbd3
  7. System.out.println(s3 == s4); // fasle
  8. }
  9. }

如果使用= 来进行拷贝呢?同上面的分析一致,= 传递的仍然是类对象引用,即类对象在堆内存中的地址。原对象变量和副本拥有的都是同一个对象在堆内存中的地址,因此它们指向的是相同的内容,两者是相互联系的。

如果我们实现Cloneable接口,并实现clone() ,此时Student的类定义如下:

  1. public class Student implements Cloneable{
  2. private String name;
  3. private int age;
  4. public Student() {
  5. }
  6. public Student(String name, int age) {
  7. this.name = name;
  8. this.age = age;
  9. }
  10. public String getName() {
  11. return name;
  12. }
  13. public void setName(String name) {
  14. this.name = name;
  15. }
  16. public int getAge() {
  17. return age;
  18. }
  19. public void setAge(int age) {
  20. this.age = age;
  21. }
  22. @Override
  23. public Object clone() {
  24. try {
  25. return super.clone();
  26. } catch (CloneNotSupportedException e) {
  27. return null;
  28. }
  29. }
  30. }

那么通过clone() 来进行对象拷贝在内存空间中会发生什么变化呢?它和前面提到的几种方法有什么区别呢?下面首先看一下原对象变量和副本内存地址上有什么不同:

  1. public class CloneDemo {
  2. public static void main(String[] args) {
  3. Student s6 = (Student) s4.clone();
  4. System.out.println("s4 is: " + s4 + " and s6 is: " + s6);
  5. // s4 is: Clone.Student@2503dbd3 and s6 is: Clone.Student@4b67cf4d
  6. System.out.println(s4 == s6); // false
  7. }
  8. }

从输出可以看出,两个变量保存的地址是不同的,那么在堆中是如何变现的呢?请看下图
不包含自定义类类型数据的类对象copy.png

从图中可以看出,通过clone() 拷贝得到的对象,虽然对应属性值相同,但是它们指向的不同同一个东西。说明原对象和副本对象在堆内存中拥有的是两块不同的地址空间,其中一个对象对属性值的改变并不会影响到另一个,但这样是否对象拷贝的问题就解决了呢?

3. 深拷贝

仔细看一下Student中关于成员变量的定义,发现不管是age还是name,它们的类型都是Java定义的类型。因此,如果对象中的所有数据字段都是数值或基本数据类型,拷贝这些字段是完全没有问题的。但如果对象除此之外还包含子对象的引用,对它的拷贝会发生什么呢?

定义一个Subject类,类中只有一个String类型的name:

  1. public class Subject {
  2. private String name;
  3. public Subject(String name) {
  4. this.name = name;
  5. }
  6. }

然后在Student中添加Subject类型的成员变量,并同时更新构造方法以及添加getter和setter:

  1. public class Student implements Cloneable{
  2. private String name;
  3. private int age;
  4. private Subject subject;
  5. public Student() {
  6. }
  7. public Student(String name, int age, Subject subject) {
  8. this.name = name;
  9. this.age = age;
  10. this.subject = subject;
  11. }
  12. public String getName() {
  13. return name;
  14. }
  15. public void setName(String name) {
  16. this.name = name;
  17. }
  18. public int getAge() {
  19. return age;
  20. }
  21. public void setAge(int age) {
  22. this.age = age;
  23. }
  24. public Subject getSubject() {
  25. return subject;
  26. }
  27. public void setSubject(Subject subject) {
  28. this.subject = subject;
  29. }
  30. @Override
  31. public Object clone() {
  32. try {
  33. return super.clone();
  34. } catch (CloneNotSupportedException e) {
  35. return null;
  36. }
  37. }
  38. }

如果通过new两次例化两个属性相同的对象,那么根据之前的分析,原对象变量和副本指向的是不同的对象。如果使用= 进行拷贝,传递是对象引用,那么原对象变量和副本指向的是同一个对象。而如果此时使用clone() 进行拷贝呢?我们首先看一下原变量和副本保存的内存地址:

  1. public class CloneDemo2 {
  2. public static void main(String[] args) {
  3. Subject s = new Subject("lakers");
  4. Student s1 = new Student("Forlgoen", 10, s);
  5. Student s2 = new Student("Forlgoen", 10, s);
  6. System.out.println(s1.getSubject() + " -- " + s2.getSubject());
  7. System.out.println("s1 is: " + s1 + " and s2 is: " + s2);
  8. System.out.println(s1 == s2);
  9. System.out.println("-------------");
  10. /*
  11. Clone.Subject@135fbaa4 -- Clone.Subject@135fbaa4
  12. s1 is: Clone.Student@45ee12a7 and s2 is: Clone.Student@330bedb4
  13. false
  14. */
  15. Student s3 = s1;
  16. System.out.println(s1.getSubject() + " -- " + s3.getSubject());
  17. System.out.println("s1 is: " + s1 + " and s3 is: " + s3);
  18. System.out.println(s1 == s3);
  19. System.out.println("-------------");
  20. /*
  21. Clone.Subject@135fbaa4 -- Clone.Subject@135fbaa4
  22. s1 is: Clone.Student@45ee12a7 and s3 is: Clone.Student@45ee12a7
  23. true
  24. */
  25. Student s4 = (Student) s1.clone();
  26. System.out.println(s1.getSubject() + " -- " + s4.getSubject());
  27. System.out.println("s1 is: " + s1 + " and s4 is: " + s4);
  28. System.out.println(s1 == s4);
  29. System.out.println("-------------");
  30. /*
  31. Clone.Subject@135fbaa4 -- Clone.Subject@135fbaa4
  32. s1 is: Clone.Student@45ee12a7 and s4 is: Clone.Student@2503dbd3
  33. false
  34. */
  35. }
  36. }

从输入中可以看出,两个变量保存的内存地址是不同的。那么两个变量指向的是否就是同一个对象呢?如果看过了引言中关于Python浅拷贝的分析,我们猜想答案应该是:两个变量指向的不是同一个对象!下面通过图示的方法来验证一下我们的猜想:
包含自定义类类型数据的类对象deepCopy.png

从图中可以看出s1和s4在age和name上拥有的地址空间是不同的,但是它们的subject属性指向的地址空间仍然是相同的。因此,拷贝对象的数值或基本数据类型的字段是完全没有问题的。但拷贝子对象引用时,拷贝字段得到的其实是相同子对象的另一个引用,它们的指向的内容是相同的。就是说s1对于subject的改变同样会影响s4中subject的值。

3.1 Cloneable接口实现

为了解决上面的问题,就需要使用Java中的深拷贝。如果想实现对于子对象的完全拷贝,而不只是引用的拷贝,那么就需要子对象的类定义同样要实现Cloneable接口已经实现clone() ,而且要使用的类的定义中的clone() 也要进行更新。

  1. public class Subject implements Cloneable{
  2. private String name;
  3. public Subject(String name) {
  4. this.name = name;
  5. }
  6. @Override
  7. protected Object clone() throws CloneNotSupportedException {
  8. return super.clone();
  9. }
  10. }
  1. public class Student implements Cloneable{
  2. private String name;
  3. private int age;
  4. private Subject subject;
  5. public Student() {
  6. }
  7. public Student(String name, int age, Subject subject) {
  8. this.name = name;
  9. this.age = age;
  10. this.subject = subject;
  11. }
  12. public String getName() {
  13. return name;
  14. }
  15. public void setName(String name) {
  16. this.name = name;
  17. }
  18. public int getAge() {
  19. return age;
  20. }
  21. public void setAge(int age) {
  22. this.age = age;
  23. }
  24. public Subject getSubject() {
  25. return this.subject;
  26. }
  27. public void setSubject(Subject subject) {
  28. this.subject = subject;
  29. }
  30. @Override
  31. public Object clone() {
  32. try {
  33. Student student = (Student) super.clone();
  34. student.subject = (Subject) subject.clone();
  35. return student;
  36. } catch (CloneNotSupportedException e) {
  37. return null;
  38. }
  39. }

此时我们再使用clone() 进行对象拷贝来看一下内存地址的情况:

  1. public class CloneDemo {
  2. public static void main(String[] args) {
  3. Student s4 = (Student) s1.clone();
  4. System.out.println(s1.getSubject() + " -- " + s4.getSubject());
  5. System.out.println("s1 is: " + s1 + " and s4 is: " + s4);
  6. System.out.println(s1 == s4);
  7. System.out.println("-------------");
  8. /*
  9. Clone.deepcopy.Subject@135fbaa4 -- Clone.deepcopy.Subject@2503dbd3
  10. s1 is: Clone.Student@45ee12a7 and s4 is: Clone.Student@2503dbd3
  11. false
  12. */
  13. }
  14. }

从输出中可以看出,它的指向的是不同的堆内存空间。然后我们再来看一下在内存中是怎么样的,是否还会有浅拷贝中的问题?
包含自定义类类型数据的类对象deepCopy.png

从图中可以看出,s1和s4此时可以说指向的是完全不同的两块空间。它们中任何一个对于任意成员变量的改变都不会影响到另外一个,从而实现了真正意义上的深拷贝。

3.2 Serializable 接口实现

3.1的方法需要在所有的类中实现Cloneable接口,并在类中实现clone() 才能实现深拷贝。另一种方法是通过实现Serializable接口的方式,通过序列化流来实现类对象的深拷贝。为了帮助实现序列化和反序列化,我们需要将序列化后的结果保存,然后在反序列化的过程中再从保存的结果出发构建新的类对象。这里使用的是ByteArrayOutputStream和ByteArrayInputStream,ByteArrayOutputStream是字节输出流,他会在内存中创建一个字节缓冲数组,所有发送到输出流的数据都保存在该字节数组缓冲区中。ByteArrayInputStream是字节数组输入流,它会在内存中创建一个字节数组缓冲区,从输入流读取的数据保存在该字节数组缓冲区中。

Serializable接口和Cloneable接口一样,它也是一个标记型接口,接口中并没有抽象方法,仅仅起到一个标记的作用。

序列化和反序列化流

  1. import java.io.Serializable;
  2. public class Subject implements Serializable, Cloneable {
  3. private String name;
  4. public Subject(String name) {
  5. this.name = name;
  6. }
  7. @Override
  8. protected Object clone() throws CloneNotSupportedException {
  9. return super.clone();
  10. }
  11. }
  1. import java.io.Serializable;
  2. public class Student implements Serializable, Cloneable {
  3. private String name;
  4. private int age;
  5. private Subject subject;
  6. public Student() {
  7. }
  8. public Student(String name, int age, Subject subject) {
  9. this.name = name;
  10. this.age = age;
  11. this.subject = subject;
  12. }
  13. public String getName() {
  14. return name;
  15. }
  16. public void setName(String name) {
  17. this.name = name;
  18. }
  19. public int getAge() {
  20. return age;
  21. }
  22. public void setAge(int age) {
  23. this.age = age;
  24. }
  25. public Subject getSubject() {
  26. return this.subject;
  27. }
  28. public void Subject(Subject subject) {
  29. this.subject = subject;
  30. }
  31. @Override
  32. public Object clone() {
  33. try {
  34. Student student = (Student) super.clone();
  35. student.subject = (Subject) subject.clone();
  36. return student;
  37. } catch (CloneNotSupportedException e) {
  38. return null;
  39. }
  40. }
  41. }
package Clone.deepcopy;

import java.io.*;

public class SerializedClone {
    public static void main(String[] args) throws IOException {
        Student s = new Student("Forlogen", 10, new Subject("Lakers"));
        Student cs = clone(s);
        System.out.println("s's address is: " + s);
        System.out.println("cs's address is: " + cs);
        System.out.println(s == cs);
        System.out.println("----------------");

        System.out.println(s.getSubject());
        System.out.println(cs.getSubject());
        System.out.println(s.getSubject() == cs.getSubject());
    }


    @SuppressWarnings("unchecked")
    public static <T extends Serializable> T clone(T obj) throws IOException {
        T cloneObj = null;
        ByteArrayOutputStream out = null;
        ObjectOutputStream obs = null;
        ByteArrayInputStream ios = null;
        ObjectInputStream ois = null;
        try{
            // 写入字节流
            out = new ByteArrayOutputStream();
            obs = new ObjectOutputStream(out);
            obs.writeObject(obj);

            // 分配内存写入原始对象,生成新对象
            ios = new ByteArrayInputStream(out.toByteArray());
            ois = new ObjectInputStream(ios);
            cloneObj = (T) ois.readObject();
            return cloneObj;
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            out.close();
            obs.close();
            ios.close();
            ois.close();
        }
        return null;
    }
}

输出结果为:

s's address is: Clone.deepcopy.Student@7ea987ac
cs's address is: Clone.deepcopy.Student@6f496d9f
false
----------------
Clone.deepcopy.Subject@61bbe9ba
Clone.deepcopy.Subject@723279cf
false

从输出结果可以看出,通过序列化和反序列化得到的类对象和原类对象是完全不同的,而且是从外到里都是不同的。同样的,我们可以通过图示的方法来进行验证,如下所示:

4. 总结

最后我们总结一下Java中浅拷贝和深拷贝的一些特点,对于浅拷贝来说:

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

而对于深拷贝来说:

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

5. 参考

Java 浅拷贝和深拷贝