Clone詳解

1.正確的克隆對象

當(dāng)需要拷貝一個對象時,很多人建議不使用Java本身的clone方法,理由之一是:正確的實(shí)現(xiàn)clone不太容易。的確如此,正確的實(shí)現(xiàn)對象的clone,有以下幾個步驟:

  1. 待Clone的對象需要實(shí)現(xiàn)Cloneable接口。
  2. 覆蓋Objectprotected Object clone()方法為待Clone對象的public Object clone()方法。
  3. 待Clone對象及其子類的clone()方法里需要調(diào)用super.clone()方法并處理CloneNotSupportedException異常。

一個clone()方法的正確實(shí)現(xiàn)如下所示:

class Room implements Cloneable{
    private String name = "matrix";
    private int price = 500;

    public Object clone() {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            // 由于實(shí)現(xiàn)了Cloneable接口,那么永不發(fā)生
        }
        return null;
    }
}

2. clone存在的問題和原因

重新審視代碼,卻會發(fā)現(xiàn)一些奇怪的地方。
首先,接口Cloneable只是一個標(biāo)記接口,其中沒有任何方法,但是接口文檔表明,如果待Clone對象不實(shí)現(xiàn)該接口,就會拋出CloneNotSupportedException異常。解答該問題,需要深入JDK源碼Object的clone()方法,截取以下片段說明:

    // Check if class of obj supports the Cloneable interface.
    // All arrays are considered to be cloneable (See JLS 20.1.5)
    // 檢查對象是否實(shí)現(xiàn)了Cloneable接口(數(shù)組默認(rèn)實(shí)現(xiàn)Cloneable)
    if (!klass->is_cloneable()) {
        ResourceMark rm(THREAD);
        THROW_MSG_0(vmSymbols::java_lang_CloneNotSupportedException(), klass->external_name());
    }
    
    // Make shallow object copy
    const int size = obj->size();
    oop new_obj_oop = NULL;
    // 分配空間
    if (obj->is_javaArray()) {
        const int length = ((arrayOop)obj())->length();
        new_obj_oop = CollectedHeap::array_allocate(klass, size, length, CHECK_NULL);
    } else {
        new_obj_oop = CollectedHeap::obj_allocate(klass, size, CHECK_NULL); 
    }
    // 具體的拷貝過程
    Copy::conjoint_jlongs_atomic((jlong*)obj(), (jlong*)new_obj_oop,
                               (size_t)align_object_size(size) / HeapWordsPerLong);

clone()方法并沒有聲明在Cloneable中從而使用Java自有的接口語言特性實(shí)現(xiàn),而是在clone()方法的底層硬編碼建立和接口的聯(lián)系。沒有使用接口語言特性,這是clone()不好用的一大原因。
其次,Object類的clone()方法的訪問權(quán)限聲明為protected,而待Clone對象需要覆蓋聲明為public。一個不可考的原因是:Java在互聯(lián)網(wǎng)發(fā)展時期,遇到了某些安全性問題,一些對象并不希望能被克?。ū热缬脩舻拿艽a),由此,將Objectclone()方法的權(quán)限由public降低為protected,從而使對象默認(rèn)不具有Clone能力,以便提高安全性。
最后,需要在待Clone對象中約定調(diào)用super.clone()。原因正是要最終調(diào)用Object中的clone()方法,以便執(zhí)行具體的克隆過程。

3. 淺拷貝和深拷貝

明白了這些,感覺很開心,繼續(xù)擴(kuò)充代碼,在房子里開一扇窗:

class Window implements Cloneable{
    private int width = 200;
    private int height = 300;

    public Object clone() {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            // never happen
        }
        return null;
    }
}

class Room implements Cloneable{
    private String name = "matrix";
    private int price = 12;
    Window window = new Window();

    // clone方法相同省略
}

愉快的克隆一間房子:

public static void main(String[] args) {
    Room room = new Room();
    Room clone = (Room) room.clone();

    System.out.println(room != clone); // true
    System.out.println(room.window != clone.window); // false
}

結(jié)果卻讓人失望,克隆出來的新房子和老房子共享了同一扇窗子,這并不是我們希望的。回顧先前clone()方法的native源碼,其中新對象中的字節(jié)由老對象拷貝而來,而Window window = new Window()Room中存儲的是一個引用,所以拷貝的僅僅是一個引用。更官方的說法是:field by filed copy即按字段拷貝。也許你已經(jīng)聽說過,這種拷貝方式稱之為淺拷貝,是JAVA的默認(rèn)實(shí)現(xiàn)方式。與之對應(yīng)的另一種拷貝方式稱之為深拷貝,這種方式會將房子中的窗子也拷貝,所以需要額外的代碼實(shí)現(xiàn),由于窗子已經(jīng)實(shí)現(xiàn)Cloneable,所以僅需在Room中添加一行代碼:

public Object clone() {
    try {
        Room room = (Room) super.clone();
        // 窗子也需要克隆
        room.window = (Window) room.window.clone();
        return room;
    } catch (CloneNotSupportedException e) {
        // never happen
    }
    return null;
}

再次克隆一間房子,運(yùn)行結(jié)果如下,終于不用擔(dān)心鄰居關(guān)閉自家的窗戶了。

    true
    true

4.clone的精確含義

骨傲天是個我行我素的人,憑什么要遵守約定調(diào)用super.clone()呢?于是他使用魔法準(zhǔn)備克隆一間教室:

// 普通房間改造的教室,里面空空如也
class ClassRoom extends Room {
}

class Room implements Cloneable{
    private String name = "matrix";
    private int price = 12;
    Window window = new Window();

    public Object clone() { 
        Room room = new Room();
        room.window = new Window();
        return room;
    }
}

克隆開始:

public static void main(String[] args) {
    Room classRoom = new ClassRoom();
    Room cloneClass = (Room) classRoom.clone();

    System.out.println(classRoom != cloneClass); 
    System.out.println(classRoom.window != cloneClass.window); 

    System.out.println(classRoom.getClass());
    System.out.println(cloneClass.getClass());
}

克隆的結(jié)果:

    true
    true
    class clone.ClassRoom
    class clone.Room

開始地很高興,結(jié)束地很傷心,克隆出的根本不是教室,而是老房子。這不是一次成功的克隆,違背了克隆的定義。而JAVA克隆的精確定義需要滿足以下三個條件:

  1. x.clone() != x必為真
  2. 一般情況,x.clone().getClass() == x.getClass()為真
  3. 一般情況,x.clone().equals(x)為真

如果不遵守約定調(diào)用super.clone(),那么將會違背第二個條件,使得克隆出的對象與原對象不屬于同一個類型。

5.其他的解決方案

由于JAVA的clone()方法在深拷貝方面有諸多缺陷,涌現(xiàn)出了許多解決方案:

  1. Copy Constructor即提供一個可拷貝對象的構(gòu)造方法。比如在Window中提供一個如下的構(gòu)造方法:
    public Window(Window window) {
        this.width = window.width;
        this.height = window.height;
    }
  1. 序列化一個對象之后再反序列化。比如先將對象轉(zhuǎn)換為JSON字符串,然后在反序列化得到新對象。Kryo的序列化機(jī)制克隆速度更快,可以參考Kryo。
  2. 使用反射逐字段克隆對象。如Java Deep Cloning Library。

如果一個對象中只包含基本數(shù)據(jù)類型和不可變對象的引用,此種情況 下,深拷貝和淺拷貝的結(jié)果一致,那么推薦使用JAVA的clone()解決方案。

附一些關(guān)于clone的討論:

  1. Java Cloning and Types of Cloning (Shallow and Deep) in Details with Example
  2. recommended solution for deep cloning/copying an instance
  3. Java Cloning: Copy Constructors vs. Cloning
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容