浅拷贝
- 对于数据类型是基本数据类型的字段,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该字段值进行修改,不会影响另一个对象拷贝得到的数据。也就是说基本数据类型可以自动实现深拷贝。
- 对于数据类型是引用数据类型的字段,比如说字段是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该字段的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该字段都指向同一个实例。在这种情况下,在一个对象中修改该字段会影响到另一个对象的该字段值。
- 对于
String
、Integer
等不可变类,都应用了常量池技术。只要每次使用setter
方法修改了String
类型的字段值,都会在常量池中新生成一个新的字符串常量并返回一个新的引用值给字段,每次都不相同,所以不可变类在拷贝过程中的效果其实是等同于基本数据类型的。假设如果想在修改引用类型字段时达到修改其引用值的效果,那么需要:a.setB(new B())
由上图可以看到基本数据类型的字段,对其值创建了新的拷贝。而引用数据类型的字段的实例仍然是只有一份,两个对象的该字段都指向同一个实例。
类和字段
新建一个类 ShallowSchool
,其中有 String
类型的字段 name
和 level
。注意,在实现浅拷贝时,此时作为后面 ShallowStudent
类的引用类型字段的 ShallowSchool
无需实现 Clonable
接口。
public class ShallowSchool {
private String name;
private String level;
public ShallowSchool(String name, String level) {
this.name = name;
this.level = level;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLevel() {
return level;
}
public void setLevel(String level) {
this.level = level;
}
}
新建另一个类 ShallowStudent
,其中有 Integer
类型的字段 id
,String
类型的字段 name
和引用类型字段 ShallowSchool
。这个类要实现 Clonable
接口,并重写 clone
方法。
public class ShallowStudent implements Cloneable {
private Integer id;
private String name;
private ShallowSchool shallowSchool;
public ShallowStudent(Integer id, String name, ShallowSchool shallowSchool) {
this.id = id;
this.name = name;
this.shallowSchool = shallowSchool;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public ShallowSchool getShallowSchool() {
return shallowSchool;
}
public void setShallowSchool(ShallowSchool shallowSchool) {
this.shallowSchool = shallowSchool;
}
@Override
protected ShallowStudent clone() throws CloneNotSupportedException {
return (ShallowStudent) super.clone();
}
}
其重写的 clone
方法中调用了默认父类 Object
的 clone
方法
protected native Object clone() throws CloneNotSupportedException;
但断点进入后发现 super
对象是 ShallowStudent
对象本身,与 this
为同一对象
单元测试
对浅克隆做全面的单元测试,以了解其特性。
@Test
public void shallowCopy() throws CloneNotSupportedException {
ShallowSchool school = new ShallowSchool("CUG", "211");
ShallowStudent student = new ShallowStudent(1, "Jake Weng", school);
ShallowStudent shallowClonedStudent = student.clone();
assertSame(student.getName(), shallowClonedStudent.getName());
assertSame(student.getShallowSchool(), shallowClonedStudent.getShallowSchool());
assertNotSame(student, shallowClonedStudent);
school.setName("WHUT");
assertSame(student.getShallowSchool(), shallowClonedStudent.getShallowSchool());
ShallowSchool anotherSchool = new ShallowSchool("WHUT", "211");
student.setShallowSchool(anotherSchool);
assertNotSame(student.getShallowSchool(), shallowClonedStudent.getShallowSchool());
student.setName("Weng ZhengKai");
assertNotEquals(student.getName(), shallowClonedStudent.getName());
assertSame("Jake Weng", shallowClonedStudent.getName());
assertSame("Weng ZhengKai", student.getName());
String englishName = new String("Jake Weng");
String chineseName = new String("Weng ZhengKai");
assertNotSame(englishName, shallowClonedStudent.getName());
assertNotSame(chineseName, student.getName());
assertEquals(englishName, shallowClonedStudent.getName());
assertEquals(chineseName, student.getName());
student.setId(2);
assertNotEquals(student.getId(), shallowClonedStudent.getId());
assertSame(1, shallowClonedStudent.getId());
assertSame(2, student.getId());
Integer oldStudentId = new Integer(1);
Integer newStudentId = new Integer(2);
assertNotSame(oldStudentId, shallowClonedStudent.getId());
assertNotSame(newStudentId, student.getId());
assertEquals(oldStudentId, shallowClonedStudent.getId());
assertEquals(newStudentId, student.getId());
}
以上 shallowCopy()
方法中的所有断言均能够通过。
以上单元测试可以验证以下结论:
- 浅拷贝得到的引用类型字段与原字段确实是共享同一引用,若两者其中之一又对自己内部的字段做了修改,那么另一引用类型字段可以感知到这种修改。
- 克隆得到的对象与原对象不是同一引用,这一点与等号直接赋值不同。
- 改变原对象(克隆对象)的不可变类型字段的值并不会影响克隆对象(原对象)的不可变类型字段。
- 改变不可变字段的值,如果改变的值不存在,那么是新建了一个常量放入池中,并将该常量的引用返回给不可变字段;如果改变的值已存在,那么是将该常量的引用直接返回给不可变字段。
- 由于 JVM 常量池的存在,所以
String
类型的字段对比能够通过assertSame
的断言测试。 - 如果使用构造方法(new)来创建一个不可变对象,即使其值在常量池中已存在,仍相当于在常量池中新建了一个常量并返回其引用。
- 如果想在修改引用类型字段时达到和修改不可变类型字段一样的效果,那么需要
a.setB(new B())
深拷贝(重写 clone 方法)
首先介绍对象图的概念。设想一下,一个类有一个对象,其字段中又有一个对象,该对象指向另一个对象,另一个对象又指向另一个对象,直到一个确定的实例。这就形成了对象图。那么,对于深拷贝来说,不仅要复制对象的所有基本数据类型的字段值,还要为所有引用数据类型的字段申请存储空间,并复制每个引用数据类型字段所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象图进行拷贝。
简单地说,深拷贝对引用数据类型的字段的对象图中所有的对象都开辟了内存空间;而浅拷贝只是传递地址指向,新的对象并没有对引用数据类型创建内存空间。
深拷贝模型如图所示,可以看到所有的字段都进行了复制。
类和字段
同之前浅拷贝的代码,新建 DeepSchool
和 DeepStudent
两个类及其相应字段。
public class DeepSchool implements Cloneable {
private String name;
private String level;
public DeepSchool(String name, String level) {
this.name = name;
this.level = level;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLevel() {
return level;
}
public void setLevel(String level) {
this.level = level;
}
@Override
public DeepSchool clone() throws CloneNotSupportedException {
return (DeepSchool) super.clone();
}
}
public class DeepStudent implements Cloneable {
private Integer id;
private String name;
private DeepSchool deepSchool;
public DeepStudent(Integer id, String name, DeepSchool deepSchool) {
this.id = id;
this.name = name;
this.deepSchool = deepSchool;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public DeepSchool getDeepSchool() {
return deepSchool;
}
public void setDeepSchool(DeepSchool deepSchool) {
this.deepSchool = deepSchool;
}
@Override
protected DeepStudent clone() throws CloneNotSupportedException {
DeepStudent deepStudent = (DeepStudent) super.clone();
deepStudent.deepSchool = deepSchool.clone();
return deepStudent;
}
}
在深拷贝中,DeepSchool
及 DeepStudent
都实现了 Cloneable
接口。DeepSchool
重写的 clone
方法中仅仅调用了 super.clone()
;而 DeepStudent
的 clone
方法不仅调用了 super.clone()
,还将字段 deepSchool
的克隆结果赋值给克隆出的 deepStudent
的 deepSchool
字段。由此可以看出,不仅被克隆的类要实现 Cloneable
接口,而且其引用类型字段的类也实现 Cloneable
接口,并且在被克隆的类重写的 clone()
方法中将引用类型的字段一一克隆、并赋值给自己的克隆对象,才能做到深拷贝。如果一个类中的引用类型字段过多,那么采用这种方式进行深拷贝会非常麻烦,因为每个引用类都要实现 Cloneable
接口并重写 clone()
方法,而且自身的 clone()
方法代码会非常冗长。
单元测试
@Test
public void deepCopy() throws CloneNotSupportedException {
DeepSchool school = new DeepSchool("CUG", "211");
DeepStudent student = new DeepStudent(2, "Jake Weng", school);
DeepStudent deepClonedStudent = student.clone();
assertSame(student.getId(), deepClonedStudent.getId());
assertSame(student.getName(), deepClonedStudent.getName());
assertSame(student.getDeepSchool().getName(), deepClonedStudent.getDeepSchool().getName());
assertSame(student.getDeepSchool().getLevel(), deepClonedStudent.getDeepSchool().getLevel());
assertNotSame(student.getDeepSchool(), deepClonedStudent.getDeepSchool());
assertNotSame(student, deepClonedStudent);
school.setName("WHUT");
assertNotSame(student.getDeepSchool(), deepClonedStudent.getDeepSchool());
assertSame("WHUT", student.getDeepSchool().getName());
assertSame("CUG", deepClonedStudent.getDeepSchool().getName());
DeepStudent anotherStudent = new DeepStudent(2, "Jake Weng", new DeepSchool("WHUT", "211"));
assertSame(student.getId(), anotherStudent.getId());
assertSame(student.getName(), anotherStudent.getName());
assertSame(student.getDeepSchool().getName(), anotherStudent.getDeepSchool().getName());
assertSame(student.getDeepSchool().getLevel(), anotherStudent.getDeepSchool().getLevel());
assertNotSame(student.getDeepSchool(), anotherStudent.getDeepSchool());
assertNotSame(student, anotherStudent);
}
由单元测试得出结论:
- 由于 JVM 常量池的存在,所以对比不可变类字段的
assertSame
断言可以通过。 - 深拷贝之后,原对象与克隆对象中的引用类型字段不再共享同一引用。
深拷贝(序列化)
与上述实现 Cloneable
并在对象及其所有引用类型字段中重写 clone()
方法不同,这种方法只需实现 Serializable
接口即可,随后使用 Java 中的 I/O 流进行深拷贝。
类和字段
public class SerializableSchool implements Serializable {
private String name;
private String level;
public SerializableSchool(String name, String level) {
this.name = name;
this.level = level;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLevel() {
return level;
}
public void setLevel(String level) {
this.level = level;
}
}
public class SerializableStudent implements Serializable {
private Integer id;
private String name;
private SerializableSchool school;
public SerializableStudent(Integer id, String name, SerializableSchool school) {
this.id = id;
this.name = name;
this.school = school;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public SerializableSchool getSchool() {
return school;
}
public void setSchool(SerializableSchool school) {
this.school = school;
}
}
单元测试
@Test
public void serializableDeepCopyWithObjectOutputStream() {
SerializableSchool school = new SerializableSchool("CUG", "211");
SerializableStudent student = new SerializableStudent(1, "Jake Weng", school);
try {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("student.txt"));
objectOutputStream.writeObject(student);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("student.txt"));
SerializableStudent readStudent = (SerializableStudent) objectInputStream.readObject();
assertSame(1, student.getId());
assertNotSame(1, readStudent.getId());
assertSame("Jake Weng", student.getName());
assertNotSame("Jake Weng", readStudent.getName());
// 反序列化后的字段与原字段的值相等,但却不是同一引用。
assertEquals(student.getId(), readStudent.getId());
assertEquals(student.getName(), readStudent.getName());
assertEquals(student.getSchool().getName(), readStudent.getSchool().getName());
assertEquals(student.getSchool().getLevel(), readStudent.getSchool().getLevel());
assertNotSame(student.getId(), readStudent.getId());
assertNotSame(student.getName(), readStudent.getName());
assertNotSame(student.getSchool().getName(), readStudent.getSchool().getName());
assertNotSame(student.getSchool().getLevel(), readStudent.getSchool().getLevel());
assertNotSame(student.getSchool(), readStudent.getSchool());
assertNotSame(student, readStudent);
student.setName("Weng ZhengKai");
school.setName("WHUT");
assertNotEquals(student.getName(), readStudent.getName());
assertNotEquals(student.getSchool().getName(), readStudent.getSchool().getName());
readStudent.setName("Weng ZhengKai");
assertSame(student.getName(), readStudent.getName());
} catch (Exception e) {
e.printStackTrace();
}
}
@Test
public void serializableDeepCopyWithByteArrayOutputStream() {
SerializableSchool school = new SerializableSchool("CUG", "211");
SerializableStudent student = new SerializableStudent(1, "Jake Weng", school);
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(student);
objectOutputStream.close();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
SerializableStudent readStudent = (SerializableStudent) objectInputStream.readObject();
assertSame(1, student.getId());
assertNotSame(1, readStudent.getId());
assertSame("Jake Weng", student.getName());
assertNotSame("Jake Weng", readStudent.getName());
// 反序列化后的字段与原字段的值相等,但却不是同一引用。
assertEquals(student.getId(), readStudent.getId());
assertEquals(student.getName(), readStudent.getName());
assertEquals(student.getSchool().getName(), readStudent.getSchool().getName());
assertEquals(student.getSchool().getLevel(), readStudent.getSchool().getLevel());
assertNotSame(student.getId(), readStudent.getId());
assertNotSame(student.getName(), readStudent.getName());
assertNotSame(student.getSchool().getName(), readStudent.getSchool().getName());
assertNotSame(student.getSchool().getLevel(), readStudent.getSchool().getLevel());
assertNotSame(student.getSchool(), readStudent.getSchool());
assertNotSame(student, readStudent);
student.setName("Weng ZhengKai");
school.setName("WHUT");
assertNotEquals(student.getName(), readStudent.getName());
assertNotEquals(student.getSchool().getName(), readStudent.getSchool().getName());
readStudent.setName("Weng ZhengKai");
assertSame(student.getName(), readStudent.getName());
} catch (Exception e) {
e.printStackTrace();
}
}
由上述单元测试可知使用 I/O 流进行对象的序列化和反序列化有两种方式:
ObjectOutputStream
&ObjectInputStream
ByteArrayOutputStream
&ByteArrayInputStream
而由单元测试的结果可以得出以下结论:
- 进行反序列化后得到的克隆对象中不仅引用类型字段与原对象完全独立,互不影响;甚至连不可变字段的常量池也不再与原对象共享,即使用序列化深拷贝后的不可变字段只是值相等,而引用不相等。
- 使用
setter
方法改变序列化深拷贝的对象的不可变字段值后,与原对象又开始共享常量池,所以最后的assertSame
能够通过。