關於Java的深拷貝和淺拷貝,簡單來講就是建立一個和已知對象如出一轍的對象。可能平常編碼過程當中用的很少,可是這是一個面試常常會問的問題,並且瞭解深拷貝和淺拷貝的原理,對於Java中的所謂值傳遞或者引用傳遞將會有更深的理解。html
①、經過 new 關鍵字java
這是最經常使用的一種方式,經過 new 關鍵字調用類的有參或無參構造方法來建立對象。好比 Object obj = new Object();面試
②、經過 Class 類的 newInstance() 方法數組
這種默認是調用類的無參構造方法建立對象。好比 Person p2 = (Person) Class.forName("com.ys.test.Person").newInstance();網絡
③、經過 Constructor 類的 newInstance 方法ide
這和第二種方法類時,都是經過反射來實現。經過 java.lang.relect.Constructor 類的 newInstance() 方法指定某個構造器來建立對象。測試
Person p3 = (Person) Person.class.getConstructors()[0].newInstance();this
實際上第二種方法利用 Class 的 newInstance() 方法建立對象,其內部調用仍是 Constructor 的 newInstance() 方法。編碼
④、利用 Clone 方法spa
Clone 是 Object 類中的一個方法,經過 對象A.clone() 方法會建立一個內容和對象 A 如出一轍的對象 B,clone 克隆,顧名思義就是建立一個如出一轍的對象出來。
Person p4 = (Person) p3.clone();
⑤、反序列化
序列化是把堆內存中的 Java 對象數據,經過某種方式把對象存儲到磁盤文件中或者傳遞給其餘網絡節點(在網絡上傳輸)。而反序列化則是把磁盤文件中的對象數據或者把網絡節點上的對象數據,恢復成Java對象模型的過程。
具體如何實現能夠參考我 這篇博文。
本篇博客咱們講解的是 Java 的深拷貝和淺拷貝,其實現方式正是經過調用 Object 類的 clone() 方法來完成。在 Object.class 類中,源碼爲:
protected native Object clone() throws CloneNotSupportedException;
這是一個用 native 關鍵字修飾的方法,關於native關鍵字有一篇博客專門有介紹,不理解也不要緊,只須要知道用 native 修飾的方法就是告訴操做系統,這個方法我不實現了,讓操做系統去實現。具體怎麼實現咱們不須要了解,只須要知道 clone方法的做用就是複製對象,產生一個新的對象。那麼這個新的對象和原對象是什麼關係呢?
這裏再給你們普及一個概念,在 Java 中基本類型和引用類型的區別。
在 Java 中數據類型能夠分爲兩大類:基本類型和引用類型。
基本類型也稱爲值類型,分別是字符類型 char,布爾類型 boolean以及數值類型 byte、short、int、long、float、double。
引用類型則包括類、接口、數組、枚舉等。
Java 將內存空間分爲堆和棧。基本類型直接在棧中存儲數值,而引用類型是將引用放在棧中,實際存儲的值是放在堆中,經過棧中的引用指向堆中存放的數據。
上圖定義的 a 和 b 都是基本類型,其值是直接存放在棧中的;而 c 和 d 是 String 聲明的,這是一個引用類型,引用地址是存放在 棧中,而後指向堆的內存空間。
下面 d = c;這條語句表示將 c 的引用賦值給 d,那麼 c 和 d 將指向同一塊堆內存空間。
咱們看以下這段代碼:
package com.ys.test; public class Person implements Cloneable{ public String pname; public int page; public Address address; public Person() {} public Person(String pname,int page){ this.pname = pname; this.page = page; this.address = new Address(); } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } public void setAddress(String provices,String city ){ address.setAddress(provices, city); } public void display(String name){ System.out.println(name+":"+"pname=" + pname + ", page=" + page +","+ address); } public String getPname() { return pname; } public void setPname(String pname) { this.pname = pname; } public int getPage() { return page; } public void setPage(int page) { this.page = page; } }
package com.ys.test; public class Address { private String provices; private String city; public void setAddress(String provices,String city){ this.provices = provices; this.city = city; } @Override public String toString() { return "Address [provices=" + provices + ", city=" + city + "]"; } }
這是一個咱們要進行賦值的原始類 Person。下面咱們產生一個 Person 對象,並調用其 clone 方法複製一個新的對象。
注意:調用對象的 clone 方法,必需要讓類實現 Cloneable 接口,而且覆寫 clone 方法。
測試:
@Test public void testShallowClone() throws Exception{ Person p1 = new Person("zhangsan",21); p1.setAddress("湖北省", "武漢市"); Person p2 = (Person) p1.clone(); System.out.println("p1:"+p1); System.out.println("p1.getPname:"+p1.getPname().hashCode()); System.out.println("p2:"+p2); System.out.println("p2.getPname:"+p2.getPname().hashCode()); p1.display("p1"); p2.display("p2"); p2.setAddress("湖北省", "荊州市"); System.out.println("將複製以後的對象地址修改:"); p1.display("p1"); p2.display("p2"); }
打印結果爲:
首先看原始類 Person 實現 Cloneable 接口,而且覆寫 clone 方法,它還有三個屬性,一個引用類型 String定義的 pname,一個基本類型 int定義的 page,還有一個引用類型 Address ,這是一個自定義類,這個類也包含兩個屬性 pprovices 和 city 。
接着看測試內容,首先咱們建立一個Person 類的對象 p1,其pname 爲zhangsan,page爲21,地址類 Address 兩個屬性爲 湖北省和武漢市。接着咱們調用 clone() 方法複製另外一個對象 p2,接着打印這兩個對象的內容。
從第 1 行和第 3 行打印結果:
p1:com.ys.test.Person@349319f9
p2:com.ys.test.Person@258e4566
能夠看出這是兩個不一樣的對象。
從第 5 行和第 6 行打印的對象內容看,原對象 p1 和克隆出來的對象 p2 內容徹底相同。
代碼中咱們只是更改了克隆對象 p2 的屬性 Address 爲湖北省荊州市(原對象 p1 是湖北省武漢市) ,可是從第 7 行和第 8 行打印結果來看,原對象 p1 和克隆對象 p2 的 Address 屬性都被修改了。
也就是說對象 Person 的屬性 Address,通過 clone 以後,其實只是複製了其引用,他們指向的仍是同一塊堆內存空間,當修改其中一個對象的屬性 Address,另外一個也會跟着變化。
淺拷貝:建立一個新對象,而後將當前對象的非靜態字段複製到該新對象,若是字段是值類型的,那麼對該字段執行復制;若是該字段是引用類型的話,則複製引用但不復制引用的對象。所以,原始對象及其副本引用同一個對象。
弄清楚了淺拷貝,那麼深拷貝就很容易理解了。
深拷貝:建立一個新對象,而後將當前對象的非靜態字段複製到該新對象,不管該字段是值類型的仍是引用類型,都複製獨立的一份。當你修改其中一個對象的任何內容時,都不會影響另外一個對象的內容。
那麼該如何實現深拷貝呢?Object 類提供的 clone 是隻能實現 淺拷貝的。
深拷貝的原理咱們知道了,就是要讓原始對象和克隆以後的對象所具備的引用類型屬性不是指向同一塊堆內存,這裏有三種實現思路。
既然引用類型不能實現深拷貝,那麼咱們將每一個引用類型都拆分爲基本類型,分別進行淺拷貝。好比上面的例子,Person 類有一個引用類型 Address(其實String 也是引用類型,可是String類型有點特殊,後面會詳細講解),咱們在 Address 類內部也重寫 clone 方法。以下:
Address.class:
1 package com.ys.test; 2 3 public class Address implements Cloneable{ 4 private String provices; 5 private String city; 6 public void setAddress(String provices,String city){ 7 this.provices = provices; 8 this.city = city; 9 } 10 @Override 11 public String toString() { 12 return "Address [provices=" + provices + ", city=" + city + "]"; 13 } 14 @Override 15 protected Object clone() throws CloneNotSupportedException { 16 return super.clone(); 17 } 18 19 }
Person.class 的 clone() 方法:
1 @Override 2 protected Object clone() throws CloneNotSupportedException { 3 Person p = (Person) super.clone(); 4 p.address = (Address) address.clone(); 5 return p; 6 }
測試仍是和上面同樣,咱們會發現更改了p2對象的Address屬性,p1 對象的 Address 屬性並無變化。
可是這種作法有個弊端,這裏咱們Person 類只有一個 Address 引用類型,而 Address 類沒有,因此咱們只用重寫 Address 類的clone 方法,可是若是 Address 類也存在一個引用類型,那麼咱們也要重寫其clone 方法,這樣下去,有多少個引用類型,咱們就要重寫多少次,若是存在不少引用類型,那麼代碼量顯然會很大,因此這種方法不太合適。
序列化是將對象寫到流中便於傳輸,而反序列化則是把對象從流中讀取出來。這裏寫到流中的對象則是原始對象的一個拷貝,由於原始對象還存在 JVM 中,因此咱們能夠利用對象的序列化產生克隆對象,而後經過反序列化獲取這個對象。
注意每一個須要序列化的類都要實現 Serializable 接口,若是有某個屬性不須要序列化,能夠將其聲明爲 transient,即將其排除在克隆屬性以外。
//深度拷貝 public Object deepClone() 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(); }
由於序列化產生的是兩個徹底獨立的對象,全部不管嵌套多少個引用類型,序列化都是能實現深拷貝的。