Java設(shè)計(jì)模式百例(番外) - Java的clone

本文源碼見:https://github.com/get-set/get-designpatterns/tree/master/prototype

本文是為下一篇“Java設(shè)計(jì)模式百例 - 原型模式”做鋪墊,討論一下Java中的對象克隆。本文內(nèi)容綜合了《Effective Java》、《Java與模式》以及其他網(wǎng)上相關(guān)資料,希望能夠?qū)δ灿兴鶐椭?/p>

Java中,對象的創(chuàng)建除了用new關(guān)鍵字,還可以使用既有對象的clone()方法來復(fù)制自身達(dá)到創(chuàng)建一個(gè)新對象的目的。

關(guān)于對象克隆,Java中有通用約定:

通用約定1: x.clone() != x 必須為真。

對象克隆與引用的復(fù)制是有本質(zhì)區(qū)別的,區(qū)別就在于x.clone()后產(chǎn)生的對象與x并不位于同一塊內(nèi)存上,兩者是獨(dú)立的,修改兩者任何一方的成員都不會(huì)導(dǎo)致另一方發(fā)生變化。就像克隆羊多利(Dolly)不會(huì)因?yàn)槠洹盎蚰赣H”(很遺憾,它沒有名字,我們暫且諧音基因,就叫Jane吧)受傷或死亡而受傷或死亡。代碼舉例:

Sheep.java

public class Sheep implements Cloneable {
    private String name;    //名字
    private int age;        //年齡
    private String breed;   //品種
    private EarTag earTag;  //耳牌

    // 構(gòu)造方法
    public Sheep(String name, int age, String breed, EarTag earTag) {
        this.name = name;
        this.age = age;
        this.breed = breed;
        this.earTag = earTag;
    }
    
    // getters & setters

    @Override
    public Sheep clone() throws CloneNotSupportedException {
        return (Sheep) super.clone();
    }
    
    @Override
    public String toString() {
        return this.name + "是一只" + this.age + "歲的" + this.breed + ", 它的" + this.earTag.getColor() + "色耳牌上寫著" + this.earTag.getId() + "號。";
    }
}

每只羊身上有個(gè)耳牌:

EarTag.java

public class EarTag implements Cloneable {
    private int id;         //耳牌編號
    private String color;   //耳牌顏色

    // 構(gòu)造方法
    public EarTag(int id, String color) {
        this.id = id;
        this.color = color;
    }
    
    // getters & setters
}

注意,

  1. 以上兩個(gè)類均需要實(shí)現(xiàn)Cloneable接口,否則執(zhí)行clone()方法會(huì)報(bào)CloneNotSupportedException異常。
  2. 若某個(gè)類允許其對象可以克隆,那么需要重寫clone()方法,并且聲明為public的,因?yàn)?code>Object的clone()方法是protected,無法被非子類和不在當(dāng)前包的其他類或?qū)ο笳{(diào)用。
  3. 派生類的clone()方法中,要調(diào)用super.clone(),以便能夠最終調(diào)用到Object.clone(),后者是個(gè)native方法,效率更高。

克隆過程如下:

Sheep jane = new Sheep("簡", 5, "多塞特白面綿羊", new EarTag(12345, "黃色"));
System.out.println(jane);
Sheep dolly = jane.clone();
System.out.println("克隆后...");
dolly.setName("多利");
dolly.getEarTag().setId(12346);
System.out.println(dolly);

輸出結(jié)果為:

簡是一只5歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12345號。
克隆后...
多利是一只5歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12346號。

仿佛很完美,所有的信息都克隆過來了,但是,我們在看一下jane這個(gè)對象(最后增加兩個(gè)輸出):

System.out.println(jane);
System.out.println(jane.getEarTag() == dolly.getEarTag());

輸出結(jié)果為:

簡是一只5歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12346號。
true

這就不對了,簡的耳牌號也變了,而且我們看到兩只羊的耳牌是”==“的,也就是jane.earTagdolly.earTag指向的是同一個(gè)對象。這在現(xiàn)實(shí)中是毫無道理的。可見,earTag這個(gè)成員變量是引用復(fù)制。

淺克隆

上邊例子中,最終調(diào)用到的Object.clone()就是淺克隆。所謂淺克隆,可以理解為只復(fù)制成員變量的”值“。

  1. 對于原生類型,其”值“就是實(shí)實(shí)在在的值,比如int age,是直接復(fù)制的;
  2. 對于引用類型,其”值“就是引用本身,比如EarTag earTag,引用原來指向的是”黃色編號為12345的牌子“,引用復(fù)制過來仍然是指向同樣的牌子,所以只是復(fù)制的值,而并未復(fù)制引用指向的對象;
  3. (補(bǔ)充)對于引用類型,如果引用本身指向的是不可變類,比如StringInteger等,引用指向的對象內(nèi)容是不可變的,一旦需要改變,其實(shí)就是從新new了一個(gè)對象,因此可以認(rèn)為復(fù)制了引用指向的對象。其效果”看起來“和原生類型的待遇是一樣的。

總結(jié)來說,被復(fù)制對象的所有原生類型變量和不可變類的引用都復(fù)制與原來的對象相同的值,而所有的對其他對象(不包含不可變類的對象)的引用仍然指向原來的對象。

深克隆

相對于淺克隆,更進(jìn)一步,深克隆把要復(fù)制的對象所引用的對象都復(fù)制一遍。

實(shí)現(xiàn)深克隆有兩種方式。一種是繼續(xù)利用clone()方法,另一種是利用對象序列化。

對于第一種方法,進(jìn)一步手動(dòng)將指向可變對象的引用再復(fù)制一遍即可。比如對于Sheep我們增加deepClone()方法,在該方法中明確將EarTag對象也復(fù)制一下。因此EarTag也需要重寫clone()方法。

Sheep.java增加deepClone()方法

public Sheep deepClone() throws CloneNotSupportedException {
    Sheep s = (Sheep)super.clone();
    s.setEarTag(s.getEarTag().clone());
    return s;
}

EarTag.java增加clone()方法,別忘了實(shí)現(xiàn)Cloneable接口

@Override
public EarTag clone() throws CloneNotSupportedException {
    return (EarTag) super.clone();
}

這時(shí)候再測試一遍看輸出:

簡是一只5歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12345號。
克隆后...
多利是一只6歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12346號。
簡是一只5歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12345號。
false

可見,EarTag對象也被克隆了。

這時(shí),其實(shí)還需要注意一個(gè)問題,我們這個(gè)例子中,EarTag的對象沒有指向其他對象的引用,假設(shè)有的話,是否要調(diào)用EarTagdeepClone()方法呢,如果是一個(gè)引用鏈,深度復(fù)制要達(dá)到什么樣的深度呢?是否有循環(huán)引用呢(比如EarTag中又有對Sheep的引用)?這都是在具體的使用過程中需要謹(jǐn)慎考慮的。

第二種方法是通過對象序列化來實(shí)現(xiàn)對象的深克隆。在Sheep.java中增加如下方法:

public Sheep serializedClone() throws IOException, ClassNotFoundException {
    ByteArrayOutputStream bao = new ByteArrayOutputStream();
    ObjectOutputStream oo = new ObjectOutputStream(bao);
    oo.writeObject(this);
    ByteArrayInputStream bai = new ByteArrayInputStream(bao.toByteArray());
    ObjectInputStream oi = new ObjectInputStream(bai);
    return (Sheep) oi.readObject();
}

注意的是,SheepEarTag都需要實(shí)現(xiàn)Serializable接口,以便打開對序列化的支持。

測試一下:

Sheep jane = new Sheep("簡", 5, "多塞特白面綿羊", new EarTag(12345, "黃色"));
System.out.println(jane);
Sheep dolly = jane.serializedClone();
System.out.println("克隆后...");
dolly.setName("多利");
dolly.setAge(6);
dolly.getEarTag().setId(12346);
System.out.println(dolly);

System.out.println(jane);
System.out.println(jane.getEarTag() == dolly.getEarTag());

輸出如下:

簡是一只5歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12345號。
克隆后...
多利是一只6歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12346號。
簡是一只5歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12345號。
false

可見也確實(shí)實(shí)現(xiàn)了深克隆。

通用約定2: x.clone().getClass() == x.getClass() 必須為真。

指的是克隆后的對象其類型是一致的。這一點(diǎn)沒有問題,及時(shí)在有繼承關(guān)系的情況下。

ClassA.java

public class ClassA implements Cloneable {
    public int getA() {
        return a;
    }

    public void setA(int a) {
        this.a = a;
    }

    private int a;

    @Override
    public ClassA clone() throws CloneNotSupportedException {
        return (ClassA) super.clone();
    }
}

ClassB.java(繼承ClassA)

public class ClassB extends ClassA {
    private String b;

    public String getB() {
        return b;
    }

    public void setB(String b) {
        this.b = b;
    }

    public void test() {
        System.out.println(super.getClass().getCanonicalName());
    }
}

測試一下:

ClassB b = new ClassB();
b.setA(1);
b.setB("b");
ClassB b1 = (ClassB) b.clone();
System.out.println(b1.getB());

結(jié)果為:

b

可見,即使子類沒有重寫clone()方法,只要其各層父類中有重新了publicclone()方法的,那么clone()方法都能正確克隆調(diào)起該方法的對象,且類型正確。話說回來,畢竟clone()的動(dòng)作最終都是源于Object的那個(gè)native方法的。

通用約定3: x.clone().equals(x)為真

這一條并非強(qiáng)制約束,但盡量保證做到。因?yàn)閺囊话阏J(rèn)識上來講,克隆的兩個(gè)對象雖然是不相等(==)的,但應(yīng)該是相同(equal)的。

重寫Sheep.java和EarTag.java的equals()方法:

Sheep.java

    @Override
    public boolean equals(Object obj) {
        if (obj == this)
            return true;
        if (!(obj instanceof Sheep))
            return false;
        Sheep s = (Sheep) obj;
        return s.name.equals(this.name) &&
                s.age == this.age &&
                s.breed.equals(this.breed) &&
                s.earTag.equals(this.earTag);
    }

EarTag.java

    @Override
    public boolean equals(Object obj) {
        if (obj == this)
            return true;
        if (!(obj instanceof Sheep))
            return false;
        Sheep s = (Sheep) obj;
        return s.name.equals(this.name) &&
                s.age == this.age &&
                s.breed.equals(this.breed) &&
                s.earTag.equals(this.earTag);
    }

測試一下:

Sheep jane = new Sheep("簡", 5, "多塞特白面綿羊", new EarTag(12345, "黃色"));
Sheep dolly = jane.serializedClone();
System.out.println("克隆后...");
System.out.println(jane.equals(dolly));

輸出為true,表示兩個(gè)對象是相同的。

總結(jié)

最后,我們總結(jié)一下,實(shí)現(xiàn)clone的方法:
1)在派生類中實(shí)現(xiàn)Cloneable借口;
2)在派生類中覆蓋基類的clone方法,聲明為public;
3)在派生類的clone方法中,調(diào)用super.clone();
4)若要深克隆對象,則需要增加對引用為非不可變對象的克隆。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容