Java拷貝——深拷貝與淺拷貝
阿新 • • 發佈:2020-09-03
深拷貝和淺拷貝
值型別 vs 引用型別
1 // 學生的所學專業
2 public class Major {
3 private String majorName;
4 // 專業名稱
5 private long majorId; // 專業代號
6 // ... 其他省略 ...
7 }
1 // 學生
2 public class Student {
3 private String name; // 姓名
4 private int age; // 年齡
5 private Major major; // 所學專業
6 // ... 其他省略 ...
7 }
賦值 vs 淺拷貝 vs 深拷貝
物件賦值
賦值是日常程式設計過程中最常見的操作,最簡單的比如:1 Student codeSheep = new Student();
2 Student codePig = codeSheep;
嚴格來說,這種不能算是物件拷貝,因為拷貝的僅僅只是引用關係,並沒有生成新的實際物件:
淺拷貝
淺拷貝屬於物件克隆方式的一種,重要的特性體現在這個「淺」字上。 比如我們試圖通過studen1例項,拷貝得到student2,如果是淺拷貝這種方式,大致模型可以示意成如下所示的樣子: 很明顯,值型別的欄位會複製一份,而引用型別的欄位拷貝的僅僅是引用地址,而該引用地址指向的實際物件空間其實只有一份。深拷貝
深拷貝相較於上面所示的淺拷貝,除了值型別欄位會複製一份,引用型別欄位所指向的物件,會在記憶體中也建立一個副本,就像這個樣子:淺拷貝程式碼實現
還以上文的例子來講,我想通過student1拷貝得到student2,淺拷貝的典型實現方式是:讓被複制物件的類實現Cloneable介面,並重寫clone()方法即可。 以上面的Student類拷貝為例: 1 public class Student implements Cloneable {
2 private String name; // 姓名
3 private int age; // 年齡
4 private Major major; // 所學專業
5 @Override
6 public Object clone() throws CloneNotSupportedException {
7 return super.clone();
8 }
9 // ... 其他省略 ...
10 }
然後我們寫個測試程式碼,一試便知:
1 public class Test { public static void main(String[] args) throws CloneNotSupportedException
2 { Major m = new Major("電腦科學與技術",666666);
3 Student student1 = new Student("CodeSheep",18, m );
4 // 由 student1 拷貝得到 student2
5 Student student2 = (Student) student1.clone(); System.out.println( student1 == student2 );
6 System.out.println( student1 );
7 System.out.println( student2 );
8 System.out.println( "\n");
9 // 修改student1的值型別欄位
10 student1.setAge( 35);
11 // 修改student1的引用型別欄位
12 m.setMajorName( "電子資訊工程");
13 m.setMajorId( 888888);
14 System.out.println( student1 );
15 System.out.println( student2 );
16 }
17 }
執行得到如下結果:
從結果可以看出:
- student1==student2列印false,說明clone()方法的確克隆出了一個新物件;
- 修改值型別欄位並不影響克隆出來的新物件,符合預期;
- 而修改了student1內部的引用物件,克隆物件student2也受到了波及,說明內部還是關聯在一起的
深拷貝程式碼實現
1、深度遍歷式拷貝
雖然clone()方法可以完成物件的拷貝工作,但是注意:clone()方法預設是淺拷貝行為,就像上面的例子一樣。若想實現深拷貝需覆寫clone()方法實現引用物件的深度遍歷式拷貝,進行地毯式搜尋。 所以對於上面的例子,如果想實現深拷貝,首先需要對更深一層次的引用類Major做改造,讓其也實現Cloneable介面並重寫clone()方法:1 public class Major implements Cloneable {
2
3 @Override protected Object clone() throws CloneNotSupportedException
4 {
5 return super.clone();
6 }
7 // ... 其他省略 ...
8 }
其次我們還需要在頂層的呼叫類中重寫clone方法,來呼叫引用型別欄位的clone()方法實現深度拷貝,對應到本文那就是Student類:
1 public class Student implements Cloneable {
2 @Override
3 public Object clone() throws CloneNotSupportedException
4 {
5 Student student = (Student) super.clone();
6 student.major = (Major) major.clone(); // 重要!!!
7 return student;
8 }
9 // ... 其他省略 ...
10 }
這時候上面的測試用例不變,執行可得結果:
很明顯,這時候student1和student2兩個物件就完全獨立了,不受互相的干擾。
2、利用反序列化實現深拷貝
用反序列化技術,我們也可以從一個物件深拷貝出另一個複製物件,而且這貨在解決多層套娃式的深拷貝問題時效果出奇的好。 所以我們這裡改造一下Student類,讓其clone()方法通過序列化和反序列化的方式來生成一個原物件的深拷貝副本: 1 public class Student implements Serializable {
2 private String name; // 姓名
3 private int age; // 年齡
4 private Major major; // 所學專業
5 public Student clone() {
6 try {
7 // 將物件本身序列化到位元組流
8 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
9 ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream );
10 objectOutputStream.writeObject( this);
11 // 再將位元組流通過反序列化方式得到物件副本
12 ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
return (Student) objectInputStream.readObject();
13 } catch(IOException e) {
14 e.printStackTrace();
15 } catch(ClassNotFoundException e) {
16 e.printStackTrace();
17 }
18 return null;
19 }
20 // ... 其他省略 ...
21 }
當然這種情況下要求被引用的子類(比如這裡的Major類)也必須是可以序列化的,即實現了Serializable介面:
1 public class Major implements Serializable { 2 // ... 其他省略 ... 3 }這時候測試用例完全不變,直接執行,也可以得到如下結果: 很明顯,這時候student1和student2兩個物件也是完全獨立的,不受互相的干擾,深拷貝完成。