Java深拷貝與序列化


對基本類型的變量進行拷貝非常簡單,直接賦值給另外一個對象即可:

1 int b = 50;
2 int a = b;  // 基本類型賦值

對於引用類型的變量(例如 String),情況稍微復雜一些,因為直接等號賦值只是復制了一份引用,而復制前后的兩個引用指向的是內存中的同一個對象。

要想實現引用類型的拷貝,可以通過實現 Cloneable 接口,並覆蓋其中的 clone 方法來實現。

看一個例子,首先定義一個待拷貝的 Student 類,為簡單起見,只設置了一個 name 屬性

 1 class Student implements Cloneable{
 2     private String name;
 3 
 4     public String getName() {
 5         return name;
 6     }
 7 
 8     public void setName(String name) {
 9         this.name = name;
10     }
11     
12     @Override
13     public Object clone(){
14         Student s = null;
15         try{
16             s = (Student)super.clone();
17         }catch(Exception e){
18             e.printStackTrace();
19         }
20         return s;
21     }
22 }
Student

可以看到,在 clone 方法里實際上是調用了 super.clone() 方法

接下來對這個類進行復制,只需要調用 clone 方法即可:

1 public void deepCopy(){
2     Student s1 = new Student();
3     s1.setName("zhang");
4         
5     Student s2 = (Student) s1.clone();
6     s1.setName("wang");
7     System.out.println(s1.getName());
8     System.out.println(s2.getName());
9 }
deepCopy

輸出結果為:

wang
zhang

由於s1修改了name屬性值,輸出的結果中s1和s2的name屬性並不相同,說明這兩個引用指向了不同的 Student 對象,實現了對象拷貝。

但是,如果在Student中間添加一個引用對象,那么這種拷貝方式就會產生問題。

為了說明問題,定義一個Car類,同樣只有一個name屬性:

 1 class Car{
 2     private String name;
 3 
 4     public String getName() {
 5         return name;
 6     }
 7 
 8     public void setName(String name) {
 9         this.name = name;
10     }
11 }
Car類定義

對 Student 類進行修改,添加一個 Car 類型的屬性(略去這部分代碼),在 deepCopy 方法里面對 Car 的 name 值進行修改,如下:

 1 public void deepCopy(){
 2     Student s1 = new Student();
 3     s1.setName("zhang");
 4     Car car = new Car();
 5     car.setName("Audi");
 6     s1.setCar(car);
 7         
 8     Student s2 = (Student) s1.clone();
 9     s1.setName("wang");
10     car.setName("BMW");
11     System.out.println(s1.getName());
12     System.out.println(s2.getName());
13     System.out.println(s1.getCar().getName());
14     System.out.println(s2.getCar().getName());
15 }
修改后的deepCopy

修改后的輸出結果如下:

wang
zhang
BMW
BMW

我們發現,對於 Car 類型的復制出現了問題,s1 和 s2 的Car屬性的 name 值是相同的,都是修改后的 BMW,可以推測 s1 和 s2 的 Car 屬性指向了內存中的同一個對象。通過s1.getCar() == s2.getCar() 進行驗證,輸出為 true,說明確實引用了同一個對象。

出現問題的原因是,上面的方法是淺拷貝方法。所謂淺拷貝,是指拷貝對象的時候只是對其中的基本類型屬性進行復制,而並不拷貝對象中的引用屬性。而我們想要實現的效果是連同 Student 中的引用類型屬性一起復制,這就是深拷貝。深拷貝是一個整個獨立的對象拷貝,深拷貝會拷貝所有的屬性,並拷貝屬性指向的動態分配的內存。當對象和它所引用的對象一起拷貝時即發生深拷貝。深拷貝相比於淺拷貝速度較慢並且花銷較大

為了解決這個問題,一種可行的方式是讓 Car 類也實現 Cloneable 接口,並覆蓋 clone 方法,在 Student 類的 clone 方法里加上一行代碼:

this.car = (Car)car.clone()

這樣的確能夠解決 Car 沒有復制的問題,然而如果 Student 中有多個引用類型屬性,這些對象有可能也會有其他的引用類型屬性,那么上面這種做法就要去所有的相關類都要實現 Cloneable 接口,並覆蓋 clone 方法,不僅麻煩,而且非常不利於后期維護和擴展。

一種比較優雅的做法是利用 Java 的序列化和反序列化實現深拷貝。序列化是指將對象轉換成字節序列的過程,反序列化是指將字節序列還原成對象的過程。一般在對象持久化保持或者進行網絡傳輸的時候會用到序列化。【需要注意的是 static 和 transient 類型的變量不會被序列化】

利用序列化和反序列化進行深拷貝比較簡單,只需要實現 Serializable 接口就行。我們對Student類就行修改,如下:

 1 class Student implements Serializable{
 2     
 3     //private static final long serialVersionUID = 1L;
 4     
 5     private String name;
 6     private Car car;
 7 
 8     public Car getCar() {
 9         return car;
10     }
11 
12     public void setCar(Car car) {
13         this.car = car;
14     }
15 
16     public String getName() {
17         return name;
18     }
19 
20     public void setName(String name) {
21         this.name = name;
22     }
23 }
修改后的Student

這里暫時忽略其中的 serialVersionUID 屬性,讓Car類也同樣實現 Serializable 接口,之后定義一個深拷貝的方法:

 1 public void deepCopyWithSerialize(){
 2     Student s1 = new Student();
 3     s1.setName("zhang111");
 4     Car car = new Car();
 5     car.setName("Audi");
 6     s1.setCar(car);
 7         
 8     ObjectOutputStream oo;
 9     try {
10         oo = new ObjectOutputStream (new FileOutputStream("a.txt"));
11         oo.writeObject(s1);
12         oo.close();
13             
14         ObjectInputStream ois = new ObjectInputStream(new FileInputStream("a.txt")); 
15         Student s2 = (Teacher) ois.readObject();
16             
17         s1.setName("wahah");
18         car.setName("BMW");
19         System.out.println(s1.getName());
20         System.out.println(s2.getName());
21         System.out.println(s1.getCar().getName());
22         System.out.println(s2.getCar().getName());
23     } catch (IOException e) {
24         // TODO Auto-generated catch block
25         e.printStackTrace();
26     } catch (ClassNotFoundException e) {
27         // TODO Auto-generated catch block
28         e.printStackTrace();
29     }
30         
31 }
deepCopyWithSerialize

輸出結果為:

wahah
zhang111
BMW
Audi
輸出結果

可以看出,成功實現了對象的深拷貝。這里選擇了利用文件來保存序列化的對象,也可以選擇其他的形式,例如 ByteArrayOutputStream 

1 ByteArrayOutputStream baos = new ByteArrayOutputStream();
2 ObjectOutputStream oos = new ObjectOutputStream(baos);
3 oos.writeObject(s1);           
4             
5 // 從流中讀出對象
6 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
7 ObjectInputStream  ois = new ObjectInputStream(bais)
8 Student s2 = ois.readObject();
ByteArrayOutputStream序列化

接下來解釋一下剛才忽略的 serialVersionUID,根據名字知道這是一個與對象的狀態有關的變量,如果代碼中沒有定義這樣的變量,那么在運行的時候會按照一定的方式自動生成,在反序列化的時候會對這個值進行判斷,如果兩個值不相等,會拋出 InvalidClassException 。由於計算默認的 serialVersionUID 對類的詳細信息具有較高的敏感性,一般建議在序列化的時候主動提供這個參數。

【總結】

① Cloneable 接口的 clone 方法默認是淺拷貝,需要自行覆蓋才能實現深拷貝。

② 使用 Serializable 序列化的方式實現深拷貝比較簡單,但是需要注意定義 serialVersionUID 的值,並且 static 和 transient 類型的變量不會被序列化。

【參考資料】

本文的內容主要參考了以下的博客,在此表示感謝

Benson的專欄-Java如何復制對象

田木木-深克隆

請叫我大師兄-Java 之 Serializable 序列化和反序列化的概念,作用的通俗易懂的解釋

Java中的關鍵字 transient

Java序列化之排除被序列化字段(transient/靜態變量)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM