在Python中的浅拷贝和深拷贝中我们探究了Python中关于浅拷贝和深拷贝的一些内容,对于不同类型数据的拷贝原理有了初步的了解。下面我们继续看一下同样是面向对象语言的Java中浅拷贝和深拷贝有什么联系和区别,并且从更深层次上进行理解。本文将通过图示的方法来剖析Java中对象拷贝(克隆)的相关内容。希望在阅读之后可以对这块内容有个较为清晰的认识,同时更好的理解JVM关于内存空间的划分。
1. 引入
Java中的浅拷贝和深拷贝都可以统称为对象克隆(clone),它指的是将一个对象的所有属性(成员变量)克隆到另一个有着相同类类型的对象中。如果现在将某一类型的数据进行一次拷贝,最直接的方法是什么呢?首先想到的仍然是=
:
- 如果待拷贝数据是基本数据类型变量:由于它们都是保存在栈内存中,因此,拷贝操作会为新变量直接在栈内存中开辟一块空间,并将待拷贝变量的值保存到开辟的空间中,此时原变量和副本本身就是两个不同的变量,它们指向的内存地址空间是不同的
- 如果待拷贝数据是基本类型的数组:回想我们新建数组是怎么写代码的,如果想新建一个int型数组,通常可写作
int[] arr = new int[]{1, 2, 3};
// or
int[] arr = {1, 2, 3};
从中可以看出,数组的创建需要使用new关键字。因此,数组实际保存在堆内存中,数组变量在栈内存中,它所保存的内容实际上就是堆内存地址。如果使用=
直接进行拷贝,那么此时传递的就是数组对象的引用,即堆内存地址。因此,原变量和副本指向的是同一个数组,其中一个对于数组元素的改变必然会影响另一个。
2. 浅拷贝
浅拷贝是Java中对象克隆的一种,如果想要实现对象的拷贝,类需要实现Cloneable接口,实现接口中的clone()
,并且方法的修饰符为public。在了解了基本的实现原理后,我们首先先看一下对于类对象使用=
进行拷贝有什么现象发生。
假设此时定义一个Student类,我们并不实现Cloneable接口和其中的clone()
,定义如下:
public class Student{
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
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;
}
}
如果创建两个属性值不同的类对象,它们会相等嘛?答案显然是NO!,因为它们首先属性值不同,另一个方面使用两次new创建的对象在堆内存中拥有不同的地址,所以它们是不同的两个对象。
public class CloneDemo {
public static void main(String[] args) {
Student s1 = new Student("Forlogen", 10);
Student s2 = new Student("Kobe", 24);
System.out.println("s1 is: " + s1 + " and s2 is: " + s2);
// s1 is: Clone.Student@135fbaa4 and s2 is: Clone.Student@45ee12a7
System.out.println(s1 == s2); // fasle
}
}
如果创建两个属性值相同的类对象呢?它们依然是两个不同的对象!因为,不管属性如何赋值,归根结底仍然是通过new来分别实例化的对象,因此它们的地址不同,自然对象也就不相同喽。
public class CloneDemo {
public static void main(String[] args) {
Student s3 = new Student("Forlogen", 10);
Student s4 = new Student("Forlogen", 10);
System.out.println("s3 is: " + s3 + " and s4 is: " + s4);
// s3 is: Clone.Student@330bedb4 and s4 is: Clone.Student@2503dbd3
System.out.println(s3 == s4); // fasle
}
}
如果使用=
来进行拷贝呢?同上面的分析一致,=
传递的仍然是类对象引用,即类对象在堆内存中的地址。原对象变量和副本拥有的都是同一个对象在堆内存中的地址,因此它们指向的是相同的内容,两者是相互联系的。
如果我们实现Cloneable接口,并实现clone()
,此时Student的类定义如下:
public class Student implements Cloneable{
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
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 Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
}
那么通过clone()
来进行对象拷贝在内存空间中会发生什么变化呢?它和前面提到的几种方法有什么区别呢?下面首先看一下原对象变量和副本内存地址上有什么不同:
public class CloneDemo {
public static void main(String[] args) {
Student s6 = (Student) s4.clone();
System.out.println("s4 is: " + s4 + " and s6 is: " + s6);
// s4 is: Clone.Student@2503dbd3 and s6 is: Clone.Student@4b67cf4d
System.out.println(s4 == s6); // false
}
}
从输出可以看出,两个变量保存的地址是不同的,那么在堆中是如何变现的呢?请看下图
从图中可以看出,通过clone()
拷贝得到的对象,虽然对应属性值相同,但是它们指向的不同同一个东西。说明原对象和副本对象在堆内存中拥有的是两块不同的地址空间,其中一个对象对属性值的改变并不会影响到另一个,但这样是否对象拷贝的问题就解决了呢?
3. 深拷贝
仔细看一下Student中关于成员变量的定义,发现不管是age还是name,它们的类型都是Java定义的类型。因此,如果对象中的所有数据字段都是数值或基本数据类型,拷贝这些字段是完全没有问题的。但如果对象除此之外还包含子对象的引用,对它的拷贝会发生什么呢?
定义一个Subject类,类中只有一个String类型的name:
public class Subject {
private String name;
public Subject(String name) {
this.name = name;
}
}
然后在Student中添加Subject类型的成员变量,并同时更新构造方法以及添加getter和setter:
public class Student implements Cloneable{
private String name;
private int age;
private Subject subject;
public Student() {
}
public Student(String name, int age, Subject subject) {
this.name = name;
this.age = age;
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;
}
public Subject getSubject() {
return subject;
}
public void setSubject(Subject subject) {
this.subject = subject;
}
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
}
如果通过new两次例化两个属性相同的对象,那么根据之前的分析,原对象变量和副本指向的是不同的对象。如果使用=
进行拷贝,传递是对象引用,那么原对象变量和副本指向的是同一个对象。而如果此时使用clone()
进行拷贝呢?我们首先看一下原变量和副本保存的内存地址:
public class CloneDemo2 {
public static void main(String[] args) {
Subject s = new Subject("lakers");
Student s1 = new Student("Forlgoen", 10, s);
Student s2 = new Student("Forlgoen", 10, s);
System.out.println(s1.getSubject() + " -- " + s2.getSubject());
System.out.println("s1 is: " + s1 + " and s2 is: " + s2);
System.out.println(s1 == s2);
System.out.println("-------------");
/*
Clone.Subject@135fbaa4 -- Clone.Subject@135fbaa4
s1 is: Clone.Student@45ee12a7 and s2 is: Clone.Student@330bedb4
false
*/
Student s3 = s1;
System.out.println(s1.getSubject() + " -- " + s3.getSubject());
System.out.println("s1 is: " + s1 + " and s3 is: " + s3);
System.out.println(s1 == s3);
System.out.println("-------------");
/*
Clone.Subject@135fbaa4 -- Clone.Subject@135fbaa4
s1 is: Clone.Student@45ee12a7 and s3 is: Clone.Student@45ee12a7
true
*/
Student s4 = (Student) s1.clone();
System.out.println(s1.getSubject() + " -- " + s4.getSubject());
System.out.println("s1 is: " + s1 + " and s4 is: " + s4);
System.out.println(s1 == s4);
System.out.println("-------------");
/*
Clone.Subject@135fbaa4 -- Clone.Subject@135fbaa4
s1 is: Clone.Student@45ee12a7 and s4 is: Clone.Student@2503dbd3
false
*/
}
}
从输入中可以看出,两个变量保存的内存地址是不同的。那么两个变量指向的是否就是同一个对象呢?如果看过了引言中关于Python浅拷贝的分析,我们猜想答案应该是:两个变量指向的不是同一个对象!下面通过图示的方法来验证一下我们的猜想:
从图中可以看出s1和s4在age和name上拥有的地址空间是不同的,但是它们的subject属性指向的地址空间仍然是相同的。因此,拷贝对象的数值或基本数据类型的字段是完全没有问题的。但拷贝子对象引用时,拷贝字段得到的其实是相同子对象的另一个引用,它们的指向的内容是相同的。就是说s1对于subject的改变同样会影响s4中subject的值。
3.1 Cloneable接口实现
为了解决上面的问题,就需要使用Java中的深拷贝。如果想实现对于子对象的完全拷贝,而不只是引用的拷贝,那么就需要子对象的类定义同样要实现Cloneable接口已经实现clone()
,而且要使用的类的定义中的clone()
也要进行更新。
public class Subject implements Cloneable{
private String name;
public Subject(String name) {
this.name = name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Student implements Cloneable{
private String name;
private int age;
private Subject subject;
public Student() {
}
public Student(String name, int age, Subject subject) {
this.name = name;
this.age = age;
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;
}
public Subject getSubject() {
return this.subject;
}
public void setSubject(Subject subject) {
this.subject = subject;
}
@Override
public Object clone() {
try {
Student student = (Student) super.clone();
student.subject = (Subject) subject.clone();
return student;
} catch (CloneNotSupportedException e) {
return null;
}
}
此时我们再使用clone()
进行对象拷贝来看一下内存地址的情况:
public class CloneDemo {
public static void main(String[] args) {
Student s4 = (Student) s1.clone();
System.out.println(s1.getSubject() + " -- " + s4.getSubject());
System.out.println("s1 is: " + s1 + " and s4 is: " + s4);
System.out.println(s1 == s4);
System.out.println("-------------");
/*
Clone.deepcopy.Subject@135fbaa4 -- Clone.deepcopy.Subject@2503dbd3
s1 is: Clone.Student@45ee12a7 and s4 is: Clone.Student@2503dbd3
false
*/
}
}
从输出中可以看出,它的指向的是不同的堆内存空间。然后我们再来看一下在内存中是怎么样的,是否还会有浅拷贝中的问题?
从图中可以看出,s1和s4此时可以说指向的是完全不同的两块空间。它们中任何一个对于任意成员变量的改变都不会影响到另外一个,从而实现了真正意义上的深拷贝。
3.2 Serializable 接口实现
3.1的方法需要在所有的类中实现Cloneable接口,并在类中实现clone()
才能实现深拷贝。另一种方法是通过实现Serializable接口的方式,通过序列化流来实现类对象的深拷贝。为了帮助实现序列化和反序列化,我们需要将序列化后的结果保存,然后在反序列化的过程中再从保存的结果出发构建新的类对象。这里使用的是ByteArrayOutputStream和ByteArrayInputStream,ByteArrayOutputStream是字节输出流,他会在内存中创建一个字节缓冲数组,所有发送到输出流的数据都保存在该字节数组缓冲区中。ByteArrayInputStream是字节数组输入流,它会在内存中创建一个字节数组缓冲区,从输入流读取的数据保存在该字节数组缓冲区中。
Serializable接口和Cloneable接口一样,它也是一个标记型接口,接口中并没有抽象方法,仅仅起到一个标记的作用。
import java.io.Serializable;
public class Subject implements Serializable, Cloneable {
private String name;
public Subject(String name) {
this.name = name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
import java.io.Serializable;
public class Student implements Serializable, Cloneable {
private String name;
private int age;
private Subject subject;
public Student() {
}
public Student(String name, int age, Subject subject) {
this.name = name;
this.age = age;
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;
}
public Subject getSubject() {
return this.subject;
}
public void Subject(Subject subject) {
this.subject = subject;
}
@Override
public Object clone() {
try {
Student student = (Student) super.clone();
student.subject = (Subject) subject.clone();
return student;
} catch (CloneNotSupportedException e) {
return null;
}
}
}
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()
方法,进而实现了对象的串行层层拷贝 - 深拷贝相比于浅拷贝速度较慢并且花销较大。