Java 深拷貝和淺拷貝
深拷貝(deep clone)與淺拷貝(shallow clone)
淺拷貝(淺復制、淺克隆):被復制對象的所有變量都含有與原來的對象相同的值,而所有的對其他對象的引用仍然指向原來的對象。
換言之,淺拷貝僅僅復制所考慮的對象,而不復制它所引用的對象。
深拷貝(深復制、深克隆):被復制對象的所有變量都含有與原來的對象相同的值,除去那些引用其他對象的變量。
那些引用其他對象的變量將指向被復制過的新對象,而不再是原有的那些被引用的對象。
換言之,深拷貝把要復制的對象所引用的對象都復制了一遍。
Java中對象的克隆
1.為了獲取對象的一份拷貝,我們可以利用Object類的clone()方法。
2.在派生類中覆蓋基類的clone()方法,並聲明為public。
(Object類中的clone()方法是protected的)。
在子類重寫的時候,可以擴大訪問修飾符的范圍。
3.在派生類的clone()方法中,調用super.clone()。
因為在運行時刻,Object類中的clone()識別出你要復制的是哪一個對象,然后為此對象分配空間,並進行對象的復制,將原始對象的內容一一復制到新對象的存儲空間中。
4.在派生類中實現Cloneable接口。
這個接口中沒有什么方法,只是說明作用。
注意:繼承自java.lang.Object類的clone()方法是淺復制。
Java的clone()方法
clone()方法定義在Object類中。
clone()方法將對象復制了一份並返回給調用者。拷貝具體的含義將取決於對象所在的類。
一般而言,clone()方法滿足:
1. 克隆對象與原對象不是同一個對象。即對任何的對象x:
x.clone() != x
2.克隆對象與原對象的類型一樣。即對任何的對象x:
x.clone().getClass() == x.getClass()
3.如果對象x的equals()方法定義恰當,那么下式應該成立:
x.clone().equals(x)
因為一個定義良好的equals()方法就應該是用來比較內容是否相等的。
練習程序
程序1:CloneTest1進行拷貝:

public class CloneTest1 { public static void main(String[] args) throws CloneNotSupportedException { Student student1 = new Student(); student1.setName("ZhangSan"); student1.setAge(20); Student student2 = new Student(); student2 = (Student) student1.clone(); System.out.println("拷貝得到的信息"); System.out.println(student2.getName()); System.out.println(student2.getAge()); System.out.println("-------------"); // 修改第二個對象的信息 student2.setName("LiSi"); student2.setAge(25); System.out.println("修改第二個對象的屬性為lisi,25后:"); System.out.println("第一個對象:"); System.out.println(student1.getName()); System.out.println(student1.getAge()); System.out.println("第二個對象:"); System.out.println(student2.getName()); System.out.println(student2.getAge()); System.out.println("-------------"); // 說明兩個引用student1和student2指向的是不同的對象 } } class Student implements Cloneable { private String name; private int 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() throws CloneNotSupportedException { // 注意此處要把protected改為public Object object = super.clone(); return object; } }
程序的輸出是:
拷貝得到的信息
ZhangSan
20
-------------
修改第二個對象的屬性為lisi,25后:
第一個對象:
ZhangSan
20
第二個對象:
LiSi
25
-------------
CloneTest1說明拷貝生成的是兩個對象。
程序2:CloneTest2:在Student類中加入Teacher類的引用,進行拷貝:

public class CloneTest2 { public static void main(String[] args) throws CloneNotSupportedException { Teacher teacher = new Teacher(); teacher.setName("Teacher Zhang"); teacher.setAge(40); Student2 student1 = new Student2(); student1.setName("ZhangSan"); student1.setAge(20); student1.setTeacher(teacher); Student2 student2 = (Student2) student1.clone(); System.out.println("拷貝得到的信息"); System.out.println(student2.getName()); System.out.println(student2.getAge()); System.out.println(student2.getTeacher().getName()); System.out.println(student2.getTeacher().getAge()); System.out.println("-------------"); // 修改老師的信息 teacher.setName("Teacher Zhang has changed"); System.out.println(student1.getTeacher().getName()); System.out.println(student2.getTeacher().getName()); // 兩個引用student1和student2指向不同的兩個對象 // 但是兩個引用student1和student2中的兩個teacher引用指向的是同一個對象 // 所以說明是淺拷貝 } } class Teacher implements Cloneable { private String name; private int 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; } } class Student2 implements Cloneable { private String name; private int age; private Teacher teacher; 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 Teacher getTeacher() { return teacher; } public void setTeacher(Teacher teacher) { this.teacher = teacher; } @Override public Object clone() throws CloneNotSupportedException { Object object = super.clone(); return object; } }
程序輸出:
拷貝得到的信息
ZhangSan
20
Teacher Zhang
40
-------------
Teacher Zhang has changed
Teacher Zhang has changed
CloneTest2說明Object類的clone()方法進行的是淺拷貝。
程序3:把CloneTest2改為深復制:
首先在Teacher類中加入clone()方法(必須的,因為需要借此改為public,不然無法調用),然后修改Student2類中的clone()方法,使得teacher引用也復制一份對象,然后用set方法設置回來。

public class CloneTest2 { public static void main(String[] args) throws Exception { Teacher teacher = new Teacher(); teacher.setName("Teacher Zhang"); teacher.setAge(40); Student2 student1 = new Student2(); student1.setName("ZhangSan"); student1.setAge(20); student1.setTeacher(teacher); Student2 student2 = (Student2) student1.clone(); System.out.println("拷貝得到的信息"); System.out.println(student2.getName()); System.out.println(student2.getAge()); System.out.println(student2.getTeacher().getName()); System.out.println(student2.getTeacher().getAge()); System.out.println("-------------"); // 修改老師的信息 teacher.setName("Teacher Zhang has changed"); System.out.println(student1.getTeacher().getName()); System.out.println(student2.getTeacher().getName()); // 兩個引用student1和student2指向不同的兩個對象 // 但是兩個引用student1和student2中的兩個teacher引用指向的是同一個對象 // 所以說明是淺拷貝 // 改為深復制之后,對teacher對象的修改只能影響第一個對象 } } class Teacher implements Cloneable { private String name; private int 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() throws CloneNotSupportedException { return super.clone(); } } class Student2 implements Cloneable { private String name; private int age; private Teacher teacher; 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 Teacher getTeacher() { return teacher; } public void setTeacher(Teacher teacher) { this.teacher = teacher; } @Override public Object clone() throws CloneNotSupportedException { // 淺復制時: // Object object = super.clone(); // return object; // 改為深復制: Student2 student = (Student2) super.clone(); // 本來是淺復制,現在將Teacher對象復制一份並重新set進來 student.setTeacher((Teacher) student.getTeacher().clone()); return student; } }
程序輸出:
拷貝得到的信息
ZhangSan
20
Teacher Zhang
40
-------------
Teacher Zhang has changed
Teacher Zhang
利用序列化實現深復制
上面例子中的方法實現深復制比較麻煩。
下面介紹一種全新的方法:利用序列化來做深復制。
把對象寫到流里的過程是序列化過程(Serialization),而把對象從流中讀出來的過程則叫做反序列化過程(Deserialization)。
應當指出的是,寫在流里的是對象的一個拷貝,而原對象仍然存在於JVM里面。
在Java語言里深復制一個對象,常常可以先使對象實現Serializable接口,然后把對象(實際上只是對象的一個拷貝)寫到一個流里,再從流里讀出來,便可以重建對象。
這樣做的前提是對象以及對象內部所有引用到的對象都是可串行化的,否則,就需要仔細考察那些不可串行化的對象可否設成transient,從而將其排除在復制過程之外。
注意Cloneable與Serializable接口都是marker Interface,也就是說它們只是標識接口,沒有定義任何方法。
程序4:利用序列化實現深拷貝例子:CloneTest3

import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class CloneTest3 { public static void main(String[] args) throws Exception { Teacher3 t = new Teacher3(); t.setName("Teacher Wang"); t.setAge(50); Student3 s1 = new Student3(); s1.setAge(20); s1.setName("ZhangSan"); s1.setTeacher(t); Student3 s2 = (Student3) s1.deepClone(); System.out.println("拷貝得到的信息:"); System.out.println(s2.getName()); System.out.println(s2.getAge()); System.out.println(s2.getTeacher().getName()); System.out.println(s2.getTeacher().getAge()); System.out.println("---------------------------"); // 將復制后的對象的老師信息修改一下: s2.getTeacher().setName("New Teacher Wang"); s2.getTeacher().setAge(28); System.out.println("修改了拷貝對象的教師后:"); System.out.println("拷貝對象的教師:"); System.out.println(s2.getTeacher().getName()); System.out.println(s2.getTeacher().getAge()); System.out.println("原來對象的教師:"); System.out.println(s1.getTeacher().getName()); System.out.println(s1.getTeacher().getAge()); // 由此證明序列化的方式實現了對象的深拷貝 } } class Teacher3 implements Serializable { private String name; private int 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; } } class Student3 implements Serializable { private String name; private int age; private Teacher3 teacher; 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 Teacher3 getTeacher() { return teacher; } public void setTeacher(Teacher3 teacher) { this.teacher = teacher; } public Object deepClone() throws Exception { // 序列化 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); // 反序列化 ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return ois.readObject(); } }
程序輸出:
拷貝得到的信息:
ZhangSan
20
Teacher Wang
50
---------------------------
修改了拷貝對象的教師后:
拷貝對象的教師:
New Teacher Wang
28
原來對象的教師:
Teacher Wang
50
serialVersionUID問題
當一個類實現了Serializable接口時,表明該類可被序列化,這個時候Eclipse會給出一個警告,要求你為該類定義一個字段,該字段名字為serialVersionUID,類型為long,提示信息如下:
The serializable class Teacher3 does not declare a static final serialVersionUID field of type long。
在Eclipse中有兩種生成方式:
一個是默認的1L;
private static final long serialVersionUID = 1L;
一個是根據類名、接口名、成員方法及屬性等來生成一個64位的哈希字段,比如:
private static final long serialVersionUID = -932183802511122207L;
如果你沒有考慮到兼容性的問題,就把它關掉,不過有這個功能是好的,只要任何類別實現了Serializable接口,如果沒有加入serialVersionUID,Eclipse都會給你提示,這個serialVersionUID為了讓該類別Serializable向后兼容。
如果你的對象序列化后存到硬盤上面后,你卻更改了類的field(增加或減少或改名),當你反序列化時,就會出現異常,這樣就會造成不兼容性的問題。
但當serialVersionUID相同時,它就會將不一樣的field以type的缺省值Deserialize,這個可以避開不兼容性的問題。
參考資料
聖思園張龍老師Java SE系列視頻教程。第105講-106講。