Java深拷貝與淺拷貝實際項目中用的很少,可是對於理解Java中值傳遞,引用傳遞十分重要,同時我的認爲對於理解內存模型也有幫助,何況面試中也是常常問的,因此理解深拷貝與淺拷貝是十分重要的。面試
1、Java中建立對象的方式數組
①:與構造方法有關的建立對象方式網絡
這是什麼意思呢?好比咱們new一個對象,其實就是調用對現象的有參或者無參的構造函數,反射中經過Class類的newInstance()方法,這種默認是調用類的無參構造方法建立對象以及Constructor類的newInstance方法,這幾種方式都是直接或者間接利用對象的構造函數來建立對象的。框架
②:利用Object類中clone()方法來拷貝一個對象,方法定義以下:jvm
protected native Object clone() throws CloneNotSupportedException;
看到了吧仍是一個native方法,native方法是非Java語言實現的代碼,經過JNI供Java程序調用。此處有個大致印象就能夠了,具體此方法實現是由系統底層來實現的,咱們能夠在Java層調用此方法來實現拷貝的功能。ide
③:反序列化的方式函數
序列化:能夠看作是將一個對象轉化爲二進制流的過程,經過這種方式把對象存儲到磁盤文件中或者在網絡上傳輸。測試
反序列化:能夠看作是將對象的二進制流從新讀取轉換成對象的過程。也就是將在序列化過程當中所生成的二進制串轉換成對象的過程。this
序列化的時候咱們能夠把一個對象寫入到流中,此時原對象還在jvm中,流中的對象能夠看做是原對象的一個克隆,以後咱們在經過反序列化操做,就達到了對原對象的一次拷貝。spa
2、Java中基本類型與引用類型說明
此處必須理解,對理解深拷貝,淺拷貝相當重要。
基本類型也叫做值類型,說白了就是一個個簡單的值,char、boolean、byte、short、int、long、float、double都屬於基本類型,基本類型數據引用與數據均存儲在棧區域,好比:
1 int a = 100; 2 int b = 234;
內存模型:
引用類型包括:類、接口、數組、枚舉等。引用類型數據引用存儲在棧區域,而值則存儲在堆區域,好比:
1 String c = "abc"; 2 String d = "dgfdere";
內存模型:
3、爲何要用克隆?
如今有一個Student類:
public class Student { private int age; public void setAge(int age) { this.age = age; } public int getAge() { return age; } }
項目中有一個對象複製的需求,而且新對象的改變不能影響原對象。好了,咱們擼起來袖子就開始寫了,大意以下:
1 Student s1 = new Student(); 2 s1.setAge(10); 3 Student s2 = s1; 4 System.out.println("s1:"+s1.getAge()); 5 System.out.println("s2:"+s2.getAge());
打印信息以下:
s1:10 s2:10
一看打印信息信心更加爆棚了,完成任務。拿給項目經理看,估計經理直接讓你去財務室結算工資了。。。。
上面確實算是複製了,可是後半要求呢?而且新對象的改變不能影響原對象,咱們改變代碼以下:
Student s1 = new Student(); s1.setAge(10); Student s2 = s1; System.out.println("s1:"+s1.getAge()); System.out.println("s2:"+s2.getAge()); // s2.setAge(12); System.out.println("s1:"+s1.getAge()); System.out.println("s2:"+s2.getAge());
打印信息以下:
s1:10 s2:10 s1:12 s2:12
咦?怎麼s1對象的age也改變了呢?對於稍有經驗的應該很容易理解,咱們看一下內存模型:
看到了吧,Student s2 = s1這句代碼在內存中實際上是使s1,s2指向了同一塊內存區域,因此後面s2的操做也影響了s1。
那怎麼解決這個問題呢?這裏就須要用到克隆了,克隆就是克隆一份當前對象而且保存其當前狀態,好比當前s1的age是10,那麼克隆對象的age一樣也是10,相比較咱們直接new一個對象這裏就是不一樣點之一,咱們直接new一個對象,那麼對象中屬性都是初始狀態,還須要咱們額外調用方法一個個設置比較麻煩,克隆的對象與原對象在堆內存中的地址是不一樣的,也就是兩個不相干的對象,好了,接下來咱們就該看看怎麼克隆對象了。
4、淺拷貝
克隆實現起來比較簡單,被複制的類須要實現Clonenable接口,不實現的話在調用對象的clone方法會拋出CloneNotSupportedException異常, 該接口爲標記接口(不含任何方法), 覆蓋clone()方法,方法中調用super.clone()方法獲得須要的複製對象。
接下來咱們改造Student類,以下:
1 public class Student implements Cloneable { 2 3 private int age; 4 5 public void setAge(int age) { 6 this.age = age; 7 } 8 9 public int getAge() { 10 return age; 11 } 12 13 @Override 14 protected Object clone() throws CloneNotSupportedException { 15 // 16 return super.clone(); 17 } 18 }
繼續改造代碼:
1 public class Main { 2 3 public static void main(String[] args) { 4 5 try { 6 Student s1 = new Student(); 7 s1.setAge(10); 8 Student s2 = (Student) s1.clone(); 9 System.out.println("s1:"+s1.getAge()); 10 System.out.println("s2:"+s2.getAge()); 11 // 12 s2.setAge(12); 13 System.out.println("s1:"+s1.getAge()); 14 System.out.println("s2:"+s2.getAge()); 15 } catch (CloneNotSupportedException e) { 16 // TODO Auto-generated catch block 17 e.printStackTrace(); 18 } 19 } 20 }
主要就是第8行,調用clone方法來給s2賦值,至關於對s1對象進行了克隆,咱們看下打印信息,以下:
s1:10 s2:10 s1:10 s2:12
看到了吧,s2改變其值而s1對象並無改變,如今內存模型以下:
堆內存中是有兩個對象的,s1,s2各自操做本身的對象,互不干涉。好了,到此上面的需求就解決了。
然而過了幾天,業務有所改變,須要添加學生的身份信息,信息包含身份證號碼以及住址,好吧,咱們修改邏輯,新建身份信息類:
1 public class IDCardInfo { 2 //模擬身份證號碼 3 private String number; 4 //模擬住址 5 private String address; 6 7 public String getNumber() { 8 return number; 9 } 10 11 public void setNumber(String number) { 12 this.number = number; 13 } 14 15 public String getAddress() { 16 return address; 17 } 18 19 public void setAddress(String address) { 20 this.address = address; 21 } 22 23 }
很簡單,咱們繼續修改Student類,添加身份信息屬性:
1 public class Student implements Cloneable { 2 3 private int age; 4 //添加身份信息屬性 5 private IDCardInfo cardInfo; 6 7 public void setAge(int age) { 8 this.age = age; 9 } 10 11 public int getAge() { 12 return age; 13 } 14 15 public IDCardInfo getCardInfo() { 16 return cardInfo; 17 } 18 19 public void setCardInfo(IDCardInfo cardInfo) { 20 this.cardInfo = cardInfo; 21 } 22 23 @Override 24 protected Object clone() throws CloneNotSupportedException { 25 // 26 return super.clone(); 27 } 28 }
以上沒什麼須要特別解釋的,咱們運行以下測試:
1 public static void main(String[] args) { 2 3 try { 4 5 IDCardInfo card1 = new IDCardInfo(); 6 card1.setNumber("11111111"); 7 card1.setAddress("北京市東城區"); 8 Student s1 = new Student(); 9 s1.setAge(10); 10 s1.setCardInfo(card1); 11 Student s2 = (Student) s1.clone(); 12 System.out.println("s1:"+s1.getAge()+","+s1.getCardInfo().getNumber()+","+s1.getCardInfo().getAddress()); 13 System.out.println("s2:"+s2.getAge()+","+s2.getCardInfo().getNumber()+","+s2.getCardInfo().getAddress()); 14 // 15 card1.setNumber("222222"); 16 card1.setAddress("北京市海淀區"); 17 s2.setAge(12); 18 System.out.println("s1:"+s1.getAge()+","+s1.getCardInfo().getNumber()+","+s1.getCardInfo().getAddress()); 19 System.out.println("s2:"+s2.getAge()+","+s2.getCardInfo().getNumber()+","+s2.getCardInfo().getAddress()); 20 } catch (CloneNotSupportedException e) { 21 // TODO Auto-generated catch block 22 e.printStackTrace(); 23 } 24 }
主要邏輯就是給s1設置IDCardInfo信息,而後克隆s1對象賦值給s2,接下來改變card1信息,咱們看下打印信息:
s1:10,11111111,北京市東城區 s2:10,11111111,北京市東城區 s1:10,222222,北京市海淀區 s2:12,222222,北京市海淀區
咦?怎麼又出問題了,咱們改變card1的信息,怎麼影響了s2對象的身份信息呢?咱們想的是隻會影響s1啊,而且咱們作了克隆技術處理。
到這裏又引出兩個概念:深拷貝與淺拷貝。
以上咱們處理的只是淺拷貝,淺拷貝會建立一個新對象,這個對象有着原始對象屬性值的一份精確拷貝。若是屬性是基本類型,拷貝的就是基本類型的值;若是屬性是引用類型,拷貝的就是引用 ,因爲拷貝的只是引用而不拷貝其對應的內存對象,因此拷貝後對象的引用類型的屬性與原對象引用類型的屬性仍是指向同一對象,引用類型的屬性對應的內存中對象不會拷貝,這裏讀起來比較繞,好好理解一下。
接下來咱們看一下上面例子的內存模型:
看到了吧,就是s1,s2中IDCardInfo引用均指向了同一塊內存地址,那怎麼解決這個問題呢?解決這個問題就須要用到深拷貝了。
5、深拷貝
Object類中的clone是隻能實現淺拷貝的,若是以上淺拷貝理解了,那麼深拷貝也不難理解,所謂深拷貝就是將引用類型以及其指向的對象內存區域也一同拷貝一份,而不只僅拷貝引用。
那怎麼實現呢?以上面例子爲例,要想實現深拷貝,那麼IDCardInfo類也要實現Cloneable接口,而且重寫clone()方法,修改以下:
1 public class IDCardInfo implements Cloneable{ 2 //模擬身份證號碼 3 private String number; 4 //模擬住址 5 private String address; 6 7 public String getNumber() { 8 return number; 9 } 10 11 public void setNumber(String number) { 12 this.number = number; 13 } 14 15 public String getAddress() { 16 return address; 17 } 18 19 public void setAddress(String address) { 20 this.address = address; 21 } 22 23 @Override 24 protected Object clone() throws CloneNotSupportedException { 25 // 26 return super.clone(); 27 } 28 }
Student中clone()修改以下:
@Override protected Object clone() throws CloneNotSupportedException { // Student stu = (Student) super.clone(); stu.cardInfo = (IDCardInfo) cardInfo.clone(); return stu; }
再次運行程序打印以下:
s1:10,11111111,北京市東城區 s2:10,11111111,北京市東城區 s1:10,222222,北京市海淀區 s2:12,11111111,北京市東城區
看到了吧,修改card1信息已經影響不到s2了,到此就實現了對象的深拷貝,此時內存模型以下:
你們想一下這樣一個情節:A對象中有B對象的引用,B對象有C對象的引用,C又有D。。。。,尤爲項目中引用三方框架中對象,要是實現深拷貝是否是特別麻煩,全部對象都要實現Cloneable接口,而且重寫clone()方法,這樣作顯然是麻煩的,那怎麼更好的處理呢?此時咱們能夠利用序列化來實現深拷貝。
6、序列化實現深拷貝
對象序列化是將對象寫到流中,反序列化則是把對象從流中讀取出來。寫到流中的對象則是原始對象的一個拷貝,原始對象還存在 JVM 中,因此咱們能夠利用對象的序列化產生克隆對象,而後經過反序列化獲取這個對象。
序列化的類都要實現Serializable接口,若是有某個屬性不須要序列化,能夠將其聲明爲transient。
接下來咱們改造源程序經過序列化來實現深拷貝,IDCardInfo以下:
public class IDCardInfo implements Serializable{ private static final long serialVersionUID = 7136686765975561495L; //模擬身份證號碼 private String number; //模擬住址 private String address; public String getNumber() { return number; } public void setNumber(String number) { this.number = number; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } }
很簡單就是讓其實現Serializable接口。
Student改造以下:
public class Student implements Serializable { private static final long serialVersionUID = 7436523253790984380L; private int age; //添加身份信息屬性 private IDCardInfo cardInfo; public void setAge(int age) { this.age = age; } public int getAge() { return age; } public IDCardInfo getCardInfo() { return cardInfo; } public void setCardInfo(IDCardInfo cardInfo) { this.cardInfo = cardInfo; } //實現深拷貝 public Object myClone() 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(); } }
一樣讓其實現Serializable接口,而且添加myClone()方法經過序列化反序列化實現其自己的深拷貝。
外部調用myClone()方法就能夠實現深拷貝了,以下:
Student s2 = (Student) s1.myClone();
運行程序:
s1:10,11111111,北京市東城區 s2:10,11111111,北京市東城區 s1:10,222222,北京市海淀區 s2:12,11111111,北京市東城區
好了到此經過序列化一樣實現了深拷貝。
7、克隆的實際應用
工做中不多用到深拷貝這塊知識,我就說一個本身工做中用到的地方,最近寫一個面向對象的網絡請求框架,框架中有一個下載的功能,咱們知道下載開始,進度更新,完畢,取消等都有相應的回調,在回調中我會傳遞出去一個下載信息的對象,這個對象包含下載文件的一些信息,好比:總長度,進度,已經下載的大小等等,這個下載信息向外傳遞就用到了克隆,咱們只傳遞當前下載信息對象的一個克隆就能夠了,千萬別把當前下載信息直接傳遞出去,試想直接傳遞出去,外界要是修改了一些信息咋辦,內部框架是會讀取一些信息的,而我只克隆一份給外界,你只須要知道當前信息就能夠了,不用你修改,你要是想修改那隨便也影響不到我內部。
好了,以上就是關於克隆技術本身的總結,以及最後說了本身工做中用到的情形,本篇到此爲止,但願對你有用。
聲明:文章將會陸續搬遷到我的公衆號,之後文章也會第一時間發佈到我的公衆號,及時獲取文章內容請關注公衆號