1. 程式人生 > 實用技巧 >Java拷貝——深拷貝與淺拷貝

Java拷貝——深拷貝與淺拷貝

深拷貝和淺拷貝


值型別 vs 引用型別

在Java中,像陣列、類Class、列舉Enum、Integer包裝類等等,就是典型的引用型別,所以操作時一般來說採用的也是引用傳遞的方式; 但是Java的語言級基礎資料型別,諸如int這些基本型別,操作時一般採取的則是值傳遞的方式,所以有時候也稱它為值型別。 為了便於下文的講述和舉例,我們這裡先定義兩個類:Student和Major,分別表示「學生」以及「所學的專業」,二者是包含關係:
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兩個物件也是完全獨立的,不受互相的干擾,深拷貝完成。