一文讀懂深克隆與淺克隆的關系


本文節選自《設計模式就該這樣學》

1 分析JDK淺克隆API帶來的問題

在Java提供的API中,不需要手動創建抽象原型接口,因為Java已經內置了Cloneable抽象原型接口,自定義的類型只需實現該接口並重寫Object.clone()方法即可完成本類的復制。
通過查看JDK的源碼可以發現,其實Cloneable是一個空接口。Java之所以提供Cloneable接口,只是為了在運行時通知Java虛擬機可以安全地在該類上使用clone()方法。而如果該類沒有實現 Cloneable接口,則調用clone()方法會拋出 CloneNotSupportedException異常。
一般情況下,如果使用clone()方法,則需滿足以下條件。

(1)對任何對象o,都有o.clone() != o。換言之,克隆對象與原型對象不是同一個對象。

(2)對任何對象o,都有o.clone().getClass() == o.getClass()。換言之,復制對象與原對象的類型一樣。

(3)如果對象o的equals()方法定義恰當,則o.clone().equals(o)應當成立。

我們在設計自定義類的clone()方法時,應當遵守這3個條件。一般來說,這3個條件中的前2個是必需的,第3個是可選的。
下面使用Java提供的API應用來實現原型模式,代碼如下。


class Client {
    public static void main(String[] args) {
        //創建原型對象
        ConcretePrototype type = new ConcretePrototype("original");
        System.out.println(type);
        //復制原型對象
        ConcretePrototype cloneType = type.clone();
        cloneType.desc = "clone";
        System.out.println(cloneType);

    }
    static class ConcretePrototype implements Cloneable {
        private String desc;

        public ConcretePrototype(String desc) {
            this.desc = desc;
        }

        @Override
        protected ConcretePrototype clone() {
            ConcretePrototype cloneType = null;
            try {
                cloneType = (ConcretePrototype) super.clone();
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return cloneType;
        }

        @Override
        public String toString() {
            return "ConcretePrototype{" +
                    "desc='" + desc + '\'' +
                    '}';
        }
    }
}

super.clone()方法直接從堆內存中以二進制流的方式進行復制,重新分配一個內存塊,因此其效率很高。由於super.clone()方法基於內存復制,因此不會調用對象的構造函數,也就是不需要經歷初始化過程。
在日常開發中,使用super.clone()方法並不能滿足所有需求。如果類中存在引用對象屬性,則原型對象與克隆對象的該屬性會指向同一對象的引用。


@Data
public class ConcretePrototype implements Cloneable {

    private int age;
    private String name;
    private List<String> hobbies;

    @Override
    public ConcretePrototype clone() {
        try {
            return (ConcretePrototype)super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }

    @Override
    public String toString() {
        return "ConcretePrototype{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", hobbies=" + hobbies +
                '}';
    }
}

修改客戶端測試代碼。


public static void main(String[] args) {
        //創建原型對象
        ConcretePrototype prototype = new ConcretePrototype();
        prototype.setAge(18);
        prototype.setName("Tom");
        List<String> hobbies = new ArrayList<String>();
        hobbies.add("書法");
        hobbies.add("美術");
        prototype.setHobbies(hobbies);
        System.out.println(prototype);
        //復制原型對象
        ConcretePrototype cloneType = prototype.clone();
        cloneType.getHobbies().add("技術控");

        System.out.println("原型對象:" + prototype);
        System.out.println("克隆對象:" + cloneType);

    }
		

我們給復制的對象新增一個屬性hobbies(愛好)之后,發現原型對象也發生了變化,這顯然不符合預期。因為我們希望復制出來的對象應該和原型對象是兩個獨立的對象,不再有聯系。從測試結果來看,應該是hobbies共用了一個內存地址,意味着復制的不是值,而是引用的地址。這樣的話,如果我們修改任意一個對象中的屬性值,protoType 和cloneType的hobbies值都會改變。這就是我們常說的淺克隆。只是完整復制了值類型數據,沒有賦值引用對象。換言之,所有的引用對象仍然指向原來的對象,顯然不是我們想要的結果。那如何解決這個問題呢?
Java自帶的clone()方法進行的就是淺克隆。而如果我們想進行深克隆,可以直接在super.clone()后,手動給復制對象的相關屬性分配另一塊內存,不過如果當原型對象維護很多引用屬性的時候,手動分配會比較煩瑣。因此,在Java中,如果想完成原型對象的深克隆,則通常使用序列化(Serializable)的方式。

2 使用序列化實現深克隆

在上節的基礎上繼續改造,增加一個deepClone()方法。


/**
 * Created by Tom.
 */
@Data
public class ConcretePrototype implements Cloneable,Serializable {

    private int age;
    private String name;
    private List<String> hobbies;

    @Override
    public ConcretePrototype clone() {
        try {
            return (ConcretePrototype)super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }

    public ConcretePrototype deepClone(){
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(this);

            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);

            return (ConcretePrototype)ois.readObject();
        }catch (Exception e){
            e.printStackTrace();
            return null;
        }


    }

    @Override
    public String toString() {
        return "ConcretePrototype{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", hobbies=" + hobbies +
                '}';
    }
}

客戶端調用代碼如下。


public static void main(String[] args) {
        //創建原型對象
        ConcretePrototype prototype = new ConcretePrototype();
        prototype.setAge(18);
        prototype.setName("Tom");
        List<String> hobbies = new ArrayList<String>();
        hobbies.add("書法");
        hobbies.add("美術");
        prototype.setHobbies(hobbies);

        //復制原型對象
        ConcretePrototype cloneType = prototype.deepCloneHobbies();
        cloneType.getHobbies().add("技術控");

        System.out.println("原型對象:" + prototype);
        System.out.println("克隆對象:" + cloneType);
        System.out.println(prototype == cloneType);


        System.out.println("原型對象的愛好:" + prototype.getHobbies());
        System.out.println("克隆對象的愛好:" + cloneType.getHobbies());
        System.out.println(prototype.getHobbies() == cloneType.getHobbies());

    }
		

運行程序,得到如下圖所示的結果,與期望的結果一致。

從運行結果來看,我們的確完成了深克隆。

關注微信公眾號『 Tom彈架構 』回復“設計模式”可獲取完整源碼。

【推薦】Tom彈架構:30個設計模式真實案例(附源碼),挑戰年薪60W不是夢

本文為“Tom彈架構”原創,轉載請注明出處。技術在於分享,我分享我快樂!
如果本文對您有幫助,歡迎關注和點贊;如果您有任何建議也可留言評論或私信,您的支持是我堅持創作的動力。關注微信公眾號『 Tom彈架構 』可獲取更多技術干貨!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM