1. 程式人生 > >理解Java淺克隆和深克隆

理解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;
}