理解Java淺克隆和深克隆
克隆概念
Java一切皆物件,克隆就是對物件的克隆;克隆可能聽起來有點高階,也可以為物件複製或者物件拷貝。
平時開發中,什麼時候需要用到物件複製呢?當你有一個實體類,有很多屬性,並且很多屬性已經賦了值,這個時候需要對這個物件進行修改操作,但後面還會用到原來的值,這時就需要物件複製。
淺克隆
用程式碼舉個栗子先:
public static class C implements Cloneable{
String name;
@Override
public String toString() {
return "C{" +
"name='" + name + '\'' +
'}';
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
@Test
public void test2() throws CloneNotSupportedException {
C c = new C();
c.name = "Cat";
System.out.println(c + " hashCode: " + c.hashCode());
System.out.println(c.name + " hashCode: " + c.name.hashCode());
C copy = (C) c.clone();
System.out.println(copy + " hashCode: " + copy.hashCode());
System.out.println(copy.name + " hashCode: " + copy.name.hashCode());
c.name = "Dog";
System.out.println(c + " hashCode: " + c.hashCode());
System.out.println(copy + " hashCode: " + copy.hashCode());
}
test2方法是個單元測試方法,執行結果:
C{name='Cat'} hashCode: 458209687
Cat hashCode: 67510
C{name='Cat'} hashCode: 233530418
Cat hashCode: 67510
C{name='Dog'} hashCode: 458209687
C{name='Cat'} hashCode: 233530418
clone()是Object的方法, 子類需要實現Cloneable介面,不實現呼叫會拋CloneNotSupportedException異常。
首先我定義了一個類C, c有個成員變數name,是String型別的。test2方法裡,先建立一個例項c,給例項的name賦值為Cat,接著列印c和c.name的hashCode; 然後用c克隆一個例項賦值給copy, 答應copy和copy.name的hashCode,對比c和copy的hashCode, 發現c和copy的hashCode是不同的,說明它倆指向的是不同的兩個例項,在堆記憶體中是有2塊區域。對比c.name和copy.name,發現它倆的hashCode是相同的. 接著吧c.name重新賦值,賦值為Dog,再看列印的log,沒有影響到copy的裡name屬性。
再看另外一個栗子:
public static class A implements Cloneable{
B b;
@Override
public String toString() {
return "A{" +
"b=" + b +
'}';
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public static class B implements Cloneable{
String name;
@Override
public String toString() {
return "B{" +
"name=" + name +
'}';
}
}
@Test
public void test() throws CloneNotSupportedException {
A a = new A();
B b = new B();
b.name = "Cat";
a.b = b;
System.out.println(a + " hashCode: " + a.hashCode());
System.out.println(a.b + " hashCode: " + a.b.hashCode());
A copy = (A) a.clone();
System.out.println(copy + " hashCode: " + copy.hashCode());
System.out.println(copy.b + " hashCode: " + copy.b.hashCode());
b.name = "Dog";
System.out.println(a + " hashCode: " + a.hashCode());
System.out.println(copy + " hashCode: " + copy.hashCode());
}
執行的結果:
A{b=B{name=Cat}} hashCode: 458209687
B{name=Cat} hashCode: 233530418
A{b=B{name=Cat}} hashCode: 683287027
B{name=Cat} hashCode: 233530418
A{b=B{name=Dog}} hashCode: 458209687
A{b=B{name=Dog}} hashCode: 683287027
前一個例子中類C中有個String的成員變數, 而這個例子中類A中有個自定義的類B成員變數, 但你會發現建立A例項, 複製一個A例項後,修改a成員變數b的name為Dog, 看日誌copy裡b的name也跟著改變啦。還沒修改為Dog之前,a和copy的hashCode是不同的,說明它倆指向的是不同的兩個例項,在堆記憶體中是有2塊區域;a.b和copy.b的hashCode是相同的,它倆指向堆記憶體中的同一區域,所以當你修改a.b.name的值, copy.b.name肯定也跟著改變。
Object的clone方法只是淺克隆, 第一個例子中, c.name = “Dog” 等同於c.name = new String(“Dog”),
c.name的引用指向已經發生了改變, 這時c.name和copy.name它倆指向的是不同的兩個例項。第二個例子中,a.b和copy.b始終都是同一個引用,
如果改為 B b1 = new B(); a.b = b1; b1.name = “Dog”, 這樣子就跟第一個例子相同。
淺克隆,對於被克隆的類中成員變數都是基本資料型別,可以實現了兩份資料;被克隆的類中成員變數是物件型別,那麼這個成員變數還是原來的引用,修改為新物件的值,舊物件的該物件型別的成員變數還是會變化。
深克隆
深克隆, 被克隆的類中成員變數無論是什麼型別, 都可以實現了兩份資料;
深克隆主要有兩個方式實現:
- 重寫clone方法
- 序列化與反序列化
重寫clone方法
重寫clone方法實現深克隆比較麻煩,要對所有是物件型別的成員變數,進行重新建立例項,重新賦值;
如果集合類更麻煩,比如說ArrayList, ArrayList的重寫了clone(), 但還是淺克隆, 要實現深克隆需要遍歷所有Model,建立例項,重新賦值。
下面程式碼是對上面淺克隆第二個兩字進行了深克隆的實現, 重寫類A的clone方法:
public static class A implements Cloneable{
B b;
@Override
public String toString() {
return "A{" +
"b=" + b +
'}';
}
@Override
public Object clone() throws CloneNotSupportedException {
A a = (A) super.clone();
B b = new B();
b.name = this.b.name;
a.b = b;
return a;
}
}
序列化與反序列化
序列化與反序列化實現深克隆方式很多,你可以將例項轉成json的字串,再將字串轉回例項,相當於複製了一份;你也可以ObjectOutputStream和ObjectInputStream實現; 在安卓上,可以利用Parcelable實現等等。
這種方式相對於重寫clone方法實現可能會簡單點,但效能上會差很多。
下面就簡單利用ObjectOutputStream和ObjectInputStream實現,程式碼如下:
static <T extends Serializable> T copy(T origin) {
ByteArrayInputStream in = null;
ByteArrayOutputStream pos = null;
T copyList = null;
try {
pos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(pos);
out.writeObject(origin);
in = new ByteArrayInputStream(pos.toByteArray());
ObjectInputStream oin = new ObjectInputStream(in);
copyList = (T) oin.readObject();
} catch (Exception ignored) {
}finally {
if(in != null){
try {
in.close();
} catch (IOException ignored) {
}
}
if(pos != null){
try {
pos.close();
} catch (IOException ignored) {
}
}
}
return copyList;
}