Java設計模式百例(番外) - Java的clone

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

本文是為下一篇“Java設計模式百例 - 原型模式”做鋪墊刀疙,討論一下Java中的對象克隆颊亮。本文內容綜合了《Effective Java》也切、《Java與模式》以及其他網上相關資料,希望能夠對您也有所幫助桶略。

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

關于對象克隆密浑,Java中有通用約定:

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

對象克隆與引用的復制是有本質區(qū)別的粗井,區(qū)別就在于x.clone()后產生的對象與x并不位于同一塊內存上,兩者是獨立的街图,修改兩者任何一方的成員都不會導致另一方發(fā)生變化浇衬。就像克隆羊多利(Dolly)不會因為其“基因母親”(很遺憾,它沒有名字餐济,我們暫且諧音基因耘擂,就叫Jane吧)受傷或死亡而受傷或死亡。代碼舉例:

Sheep.java

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

    // 構造方法
    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() + "號絮姆。";
    }
}

每只羊身上有個耳牌:

EarTag.java

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

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

注意醉冤,

  1. 以上兩個類均需要實現Cloneable接口,否則執(zhí)行clone()方法會報CloneNotSupportedException異常篙悯。
  2. 若某個類允許其對象可以克隆蚁阳,那么需要重寫clone()方法,并且聲明為public的鸽照,因為Objectclone()方法是protected螺捐,無法被非子類和不在當前包的其他類或對象調用。
  3. 派生類的clone()方法中矮燎,要調用super.clone()定血,以便能夠最終調用到Object.clone(),后者是個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);

輸出結果為:

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

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

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

輸出結果為:

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

這就不對了,簡的耳牌號也變了贤壁,而且我們看到兩只羊的耳牌是”==“的悼枢,也就是jane.earTagdolly.earTag指向的是同一個對象。這在現實中是毫無道理的脾拆÷鳎可見莹妒,earTag這個成員變量是引用復制。

淺克隆

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

  1. 對于原生類型鉴腻,其”值“就是實實在在的值,比如int age百揭,是直接復制的爽哎;
  2. 對于引用類型,其”值“就是引用本身器一,比如EarTag earTag课锌,引用原來指向的是”黃色編號為12345的牌子“,引用復制過來仍然是指向同樣的牌子祈秕,所以只是復制的值渺贤,而并未復制引用指向的對象;
  3. (補充)對于引用類型请毛,如果引用本身指向的是不可變類志鞍,比如StringInteger等获印,引用指向的對象內容是不可變的述雾,一旦需要改變,其實就是從新new了一個對象兼丰,因此可以認為復制了引用指向的對象玻孟。其效果”看起來“和原生類型的待遇是一樣的。

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

深克隆

相對于淺克隆艳丛,更進一步匣掸,深克隆把要復制的對象所引用的對象都復制一遍。

實現深克隆有兩種方式氮双。一種是繼續(xù)利用clone()方法碰酝,另一種是利用對象序列化。

對于第一種方法戴差,進一步手動將指向可變對象的引用再復制一遍即可送爸。比如對于Sheep我們增加deepClone()方法,在該方法中明確將EarTag對象也復制一下。因此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()方法墨吓,別忘了實現Cloneable接口

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

這時候再測試一遍看輸出:

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

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

這時秘症,其實還需要注意一個問題,我們這個例子中讥珍,EarTag的對象沒有指向其他對象的引用历极,假設有的話,是否要調用EarTagdeepClone()方法呢衷佃,如果是一個引用鏈,深度復制要達到什么樣的深度呢蹄葱?是否有循環(huán)引用呢(比如EarTag中又有對Sheep的引用)氏义?這都是在具體的使用過程中需要謹慎考慮的。

第二種方法是通過對象序列化來實現對象的深克隆图云。在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都需要實現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

可見也確實實現了深克隆情萤。

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

指的是克隆后的對象其類型是一致的摹恨。這一點沒有問題筋岛,及時在有繼承關系的情況下。

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());

結果為:

b

可見晒哄,即使子類沒有重寫clone()方法睁宰,只要其各層父類中有重新了publicclone()方法的,那么clone()方法都能正確克隆調起該方法的對象寝凌,且類型正確柒傻。話說回來,畢竟clone()的動作最終都是源于Object的那個native方法的较木。

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

這一條并非強制約束红符,但盡量保證做到。因為從一般認識上來講,克隆的兩個對象雖然是不相等(==)的违孝,但應該是相同(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,表示兩個對象是相同的雌桑。

總結

最后喇喉,我們總結一下,實現clone的方法:
1)在派生類中實現Cloneable借口校坑;
2)在派生類中覆蓋基類的clone方法拣技,聲明為public;
3)在派生類的clone方法中耍目,調用super.clone()膏斤;
4)若要深克隆對象,則需要增加對引用為非不可變對象的克隆邪驮。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末莫辨,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子毅访,更是在濱河造成了極大的恐慌沮榜,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喻粹,死亡現場離奇詭異蟆融,居然都是意外死亡,警方通過查閱死者的電腦和手機守呜,發(fā)現死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進店門型酥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人查乒,你說我怎么就攤上這事弥喉。” “怎么了侣颂?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵档桃,是天一觀的道長。 經常有香客問我憔晒,道長藻肄,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任拒担,我火速辦了婚禮嘹屯,結果婚禮上,老公的妹妹穿的比我還像新娘从撼。我一直安慰自己州弟,他們只是感情好钧栖,可當我...
    茶點故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著婆翔,像睡著了一般拯杠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上啃奴,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天潭陪,我揣著相機與錄音,去河邊找鬼最蕾。 笑死依溯,一個胖子當著我的面吹牛,可吹牛的內容都是我干的瘟则。 我是一名探鬼主播黎炉,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼醋拧!你這毒婦竟也來了慷嗜?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤趁仙,失蹤者是張志新(化名)和其女友劉穎洪添,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體雀费,經...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年痊焊,在試婚紗的時候發(fā)現自己被綠了盏袄。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡薄啥,死狀恐怖辕羽,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情垄惧,我是刑警寧澤刁愿,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站到逊,受9級特大地震影響铣口,放射性物質發(fā)生泄漏。R本人自食惡果不足惜觉壶,卻給世界環(huán)境...
    茶點故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一脑题、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧铜靶,春花似錦叔遂、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽痊末。三九已至,卻和暖如春哩掺,著一層夾襖步出監(jiān)牢的瞬間凿叠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工疮丛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留幔嫂,地道東北人。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓誊薄,卻偏偏與公主長得像履恩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子呢蔫,可洞房花燭夜當晚...
    茶點故事閱讀 43,697評論 2 351

推薦閱讀更多精彩內容

  • 1大同小異的工作周報 Sunny軟件公司一直使用自行開發(fā)的一套OA (Office Automatic切心,辦公自動化...
    justCode_閱讀 1,153評論 0 3
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法片吊,內部類的語法绽昏,繼承相關的語法,異常的語法俏脊,線程的語...
    子非魚_t_閱讀 31,602評論 18 399
  • 定義 原型模式屬于對象的創(chuàng)建模式全谤。通過給出一個原型對象來指明所有創(chuàng)建的對象的類型,然后用復制這個原型對象的辦法創(chuàng)建...
    步積閱讀 1,342評論 0 2
  • 微感秋意冷爷贫,夜行雙臂寒认然。 遙看壁千仞,前途萬重山漫萄。
    明哥明說閱讀 130評論 0 0
  • 廢舊紙板卷员,按著紙板紋路想了一個創(chuàng)意
    三青w閱讀 943評論 16 47