對基本類型的變量進行拷貝很是簡單,直接賦值給另一個對象便可:html
1 int b = 50; 2 int a = b; // 基本類型賦值
對於引用類型的變量(例如 String),狀況稍微複雜一些,由於直接等號賦值只是複製了一份引用,而複製先後的兩個引用指向的是內存中的同一個對象。網絡
要想實現引用類型的拷貝,能夠經過實現 Cloneable 接口,並覆蓋其中的 clone 方法來實現。ide
看一個例子,首先定義一個待拷貝的 Student 類,爲簡單起見,只設置了一個 name 屬性this
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 }
能夠看到,在 clone 方法裏其實是調用了 super.clone() 方法spa
接下來對這個類進行復制,只須要調用 clone 方法便可:.net
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 }
輸出結果爲:code
wang
zhang
因爲s1修改了name屬性值,輸出的結果中s1和s2的name屬性並不相同,說明這兩個引用指向了不一樣的 Student 對象,實現了對象拷貝。htm
可是,若是在Student中間添加一個引用對象,那麼這種拷貝方式就會產生問題。對象
爲了說明問題,定義一個Car類,一樣只有一個name屬性:blog
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 }
對 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 }
修改後的輸出結果以下:
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 }
這裏暫時忽略其中的 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 }
輸出結果爲:
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();
接下來解釋一下剛纔忽略的 serialVersionUID,根據名字知道這是一個與對象的狀態有關的變量,若是代碼中沒有定義這樣的變量,那麼在運行的時候會按照必定的方式自動生成,在反序列化的時候會對這個值進行判斷,若是兩個值不相等,會拋出 InvalidClassException 。因爲計算默認的 serialVersionUID 對類的詳細信息具備較高的敏感性,通常建議在序列化的時候主動提供這個參數。
【總結】
① Cloneable 接口的 clone 方法默認是淺拷貝,須要自行覆蓋才能實現深拷貝。
② 使用 Serializable 序列化的方式實現深拷貝比較簡單,可是須要注意定義 serialVersionUID 的值,而且 static 和 transient 類型的變量不會被序列化。
【參考資料】
本文的內容主要參考瞭如下的博客,在此表示感謝