前言
寫這篇博客的緣由是之前在做網易的筆試的時候,簡答題問了淺拷貝和深拷貝的區別和在Java的實現,因爲之前都沒了解過深,淺拷貝所以白白丟分,所以今天就查閱了資料,好好整理下深,淺拷貝的區別
深拷貝和淺拷貝
淺拷貝: 淺拷貝指的是把原對象的所有屬性都拷貝到新對象上去,如果字段是值類型的,那麼對該字段執行復制;如果該字段是引用類型的話,則複製引用但不復制引用的對象。因此,原始對象及其新對象引用同一個對象。但是String例外,爲啥我們下面會講
深拷貝: 深拷貝指的是把原對象的所有屬性都複製一份新的再拷貝到新對象上,也就是無論該字段是值類型的還是引用類型,都複製獨立的一份。當你修改其中一個對象的任何內容時,都不會影響另一個對象的內容。
實現
淺拷貝通過clone()方法實現,clone屬於Object裏的方法,也就是說所有對象都可以拷貝
但是要讓類能夠實現拷貝,類必須實現Cloneable接口,否則會拋出CloneNotSupportedException 異常
簡單來說就是在拷貝的時候,clone()會先檢查你要拷貝的類是否實現了Cloneable接口,如果沒有會拋出異常。
實現
淺拷貝
我們先寫一個Son類
public class Son implements Cloneable, Serializable {
private String name = "I am son";
private Integer age = 21;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
再寫一個Father類,類中有Son對象, 注意:必須實現Cloneable接口
public class Father implements Cloneable, Serializable {
private String name = "I am father";
private Integer age = 30;
private Son son = new Son();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Son getSon() {
return son;
}
public void setSon(Son son) {
this.son = son;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
測試:
public static void main(String[] args) {
Father father = new Father();
try {
Father ganFather = (Father) father.clone();
System.out.println("ganFather == father : "+ (ganFather == father));
System.out.println("ganFather: "+ganFather);
System.out.println("father: "+father);
System.out.println("ganFather.getName() == father.getName() : "+(ganFather.getName() == father.getName()));
System.out.println(ganFather.getName().hashCode());
System.out.println(father.getName().hashCode());
father.setName("Father");
System.out.println(father.getName()+" , "+ganFather.getName());
father.setAge(55);
System.out.println(father.getAge()+" , "+ganFather.getAge());
System.out.println("ganFather.getSon() == father.getSon() : "+(ganFather.getSon() == father.getSon()));
System.out.println("ganFather.getSon() : "+ganFather.getSon());
System.out.println("father.getSon() : "+father.getSon());
father.getSon().setName("Son");
System.out.println(father.getSon().getName()+" , "+ganFather.getSon().getName());
} catch (Exception e) {
e.printStackTrace();
}
}
結果:
從結果可以看到,父親的名字和年齡改變了,但乾爹的對應數據沒有改變,但是修改父親中兒子的名字,乾爹中兒子的名字也跟着改變了,這就是因爲淺拷貝只複製了對象的引用。
可能有人要問String也是引用類型,爲啥父親中String類型的name改變,乾爹對應的卻沒有變呢,這是因爲String是不可變的,指向常量池,因爲String的不可變性,改變name時並不是改變它本身,而是新創建了個字符串並指向他
初始:
修改name之後:
可以看到父親的引用改變了但乾爹的引用並沒有改變,所以String雖然是引用類型,但是它的改變並不會影響原始對象或者新對象。
而改變父親Son的名字,乾爹Son的名字也會改變是因爲,他們引用的是同一個對象
初始:
修改name之後:
所以修改原對象的引用類型,新對象的引用類型也會跟着改變
深拷貝
深拷貝有兩種方法
- 讓每個引用類型都重寫clone() 方法
既然引用類型不能實現深拷貝,那麼我們將每個引用類型都拆分爲基本類型,分別進行淺拷貝。比如上面的例子,Father類有一個引用類型 Son,我們在 Son類內部也重寫 clone 方法。如下:
Son類
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
Father類
@Override
protected Object clone() throws CloneNotSupportedException {
Father father = (Father) super.clone();
father.son = (Son) father.son.clone();
return father;
// return super.clone();
}
測試跟上面一樣,結果如下:
可以看到Son是兩個對象,父親的Son改變並沒有影響乾爹的Son。
這方法簡單,但是有個弊端,這裏我們Father類只有一個 Son 引用類型,而 Son 類沒有,所以我們只用重寫 Son 類的clone 方法,但是如果 Son 類也存在一個引用類型,那麼我們也要重寫其clone 方法,這樣下去,有多少個引用類型,我們就要重寫多少次,如果存在很多引用類型,那麼代碼量顯然會很大,所以這種方法不太合適。
- 序列化
序列化是將對象寫到流中便於傳輸,而反序列化則是把對象從流中讀取出來。這裏將對象序列化寫到緩衝區中,然後通過反序列化從緩衝區中獲取這個對象。
注意每個需要序列化的類都要實現 Serializable 接口,如果有某個屬性不需要序列化,可以將其聲明爲 transient,即將其排除在克隆屬性之外。
public Father deepClone() throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(this);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
return (Father) objectInputStream.readObject();
}
因爲序列化產生的是兩個完全獨立的對象,所以無論嵌套多少個引用類型,序列化都是能實現深拷貝的。