天天看點

寂然解讀設計模式 - 原型模式(下)

I walk very slowly, but I never walk backwards            

設計模式 - 原型模式(下)

​ 寂然

大家好~,我是寂然,本節課呢,我們對原型模式進行深入一點的讨論,我們來聊聊深拷貝和淺拷貝

前情提要

上節課,我們聊了聊小李求職的事情,寫了一個關于原型模式的案例,大家有沒有發現,我們使用原型模式建立類 Resume 的對象時,類 Resume 的屬性,都是 String 類型,換而言之,都是簡單類型,那假設,現在增加了一個屬性,履歷上需要有一個證明人,是 Witness 類型的對象,那大家一起來思考這樣一個問題,當再次進行原型拷貝的時候,屬性 witness 會怎麼去處理呢?是複制一份,還是讓一個引用去指向已有的 witness?覺知此事要躬行,下面,我們通過案例代碼來進行測試

案例示範

//履歷類
public class Resume implements Cloneable{

    private String name;

    private String position;

    private String salary;

    public Witness witness; //證明人

    //構造器/get/set/toString...省略
}

//證明人類
public class Witness {

    private String name;

    private String job; //職務

    //構造器/get/set/toString...省略
}

//使用原型模式測試
public class Client {

    public static void main(String[] args) {

        Resume resume = new Resume("小李", "海澱區", "面議");

        resume.setWitness(new Witness("三叔","養豬場CTO"));

        Resume clone = (Resume)resume.clone(); //使用原型模式克隆兩份

        Resume clone1 = (Resume)resume.clone();

        Resume clone2 = (Resume)resume.clone();

        System.out.println(clone.getWitness().hashCode()); //測試

        System.out.println(clone1.getWitness().hashCode());

        System.out.println(clone2.getWitness().hashCode());

    }
}           

可以看到,我們使用原型模式克隆了三份,發現三個成員變量 witness 的哈希值是相同的,也就是說,預設并沒有對屬性 witness 進行拷貝,而是通過一個引用去指向已有的 witness,這種情況就稱為淺拷貝

淺拷貝

下面,我們來一起看一下淺拷貝的介紹

對于資料類型是基本資料類型的成員變量,淺拷貝會直接進行值傳遞,就是将該屬性值複制一份給新的對象

對于資料類型是引用資料類型的成員變量,比如說成員變量是某個數組、某個類的對象等,那麼淺拷貝會進行引用傳遞,也就是隻是将該成員變量的引用值(記憶體位址)複制一份給新的對象,實際上兩個對象的該成員變量都指向同一個執行個體,這種情況下,在一個對象中修改該成員變量會影響到另一個對象的該成員變量值

是以,我們前面講的小李求職的案例就是淺拷貝,淺拷貝就是使用預設的 clone() 方法實作的

那如果說,實際情況中,我們希望對于資料類型是引用資料類型的成員變量,也複制一份呢?那對比着再來聊一下深拷貝,以及深拷貝的實作方式

深拷貝

深拷貝的基本介紹如下

複制對象的所有基本資料類型的成員變量值

為所有引用資料類型的成員變量申請存儲空間,并複制每個引用資料類型成員變量所引用的對象,直到該對象可達的所有對象,也就是說,對象進行深拷貝要對整個對象(包括對象的引用類型)進行拷貝

深拷貝在實際開發中有兩種實作方式,一種是通過重寫 clone () 方法實作,一種是通過對象序列化實作

實作方式一:重寫 clone() 方法

示例代碼如下圖所示

public class DeepCloneableTarget implements Serializable, Cloneable {

    private static final long serialVersionUID = 1L;

    private String cloneName;

    private String cloneClass;

    public DeepCloneableTarget(String cloneName, String cloneClass) {
        this.cloneName = cloneName;
        this.cloneClass = cloneClass;
    }

    //因為該類的屬性都是String,是以我們使用預設的clone方法完成即可
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class DeepProtoType implements Serializable, Cloneable {

    private String name; //String類型

    private DeepCloneableTarget deepCloneableTarget; //引用類型

    public DeepProtoType() {
        super();
    }

    //深拷貝 - 方式一:重寫clone方法
    @Override
    protected Object clone() throws CloneNotSupportedException {

        Object deep = null;

        //這裡完成的是對基本資料類型以及String類型的拷貝
        deep = super.clone();

        //對引用類型的屬性進行單獨處理
        DeepProtoType deepProtoType = (DeepProtoType) deep;

        deepProtoType.deepCloneableTarget= (DeepCloneableTarget) deepCloneableTarget.clone();

        return deepProtoType;
    }
}

public class Client {

    public static void main(String[] args) throws CloneNotSupportedException {

        DeepProtoType deepProtoType = new DeepProtoType();

        deepProtoType.name = "小李";

        deepProtoType.deepCloneableTarget = new DeepCloneableTarget("老李","克隆類");

        //方式一完成深拷貝
        DeepProtoType clone = (DeepProtoType) deepProtoType.clone();

        DeepProtoType clone1 = (DeepProtoType) deepProtoType.clone();

        System.out.println(clone.deepCloneableTarget.hashCode());

        System.out.println(clone1.deepCloneableTarget.hashCode()); //哈希值不一樣

    }
}           

可以看到,經測試,哈希值不一樣,說明确實整個對象(包括對象的引用類型)都進行了拷貝 ,實作了深拷貝

重寫 clone() 方法完成深拷貝的思路就是,分步進行,在 clone() 方法裡,首先調用 super.clone() 完成對基本類型和 String 類型的拷貝,因為對于基本資料類型的成員變量,深拷貝和淺拷貝的處理方式一緻,都是将該屬性值複制一份給新的對象,接着我們對引用類型的屬性進行單獨處理,同樣使用 clone() 方法完成屬性值的拷貝

那這樣有的小夥伴要問了,如果 DeepProtoType 有很多個引用類型的成員屬性,都要這樣挨個使用 clone() 方法完成屬性值的拷貝嘛?是的,這種情況下,每個引用類型需要單獨處理,那還有的小夥伴問了,如果引用類型是個類,裡面仍然有引用類型的成員屬性呢?

級聯處理示範

如果引用類型是個class,類型,裡面仍然有引用類型的成員屬性同樣,核心思想是分布單獨處理,假設類 DeepProtoType 裡有成員屬性 deepCloneableTarget ,而DeepCloneableTarget 類中有成員屬性 witness ,同樣使用方式一,重寫 clone() 方法完成深拷貝,示例代碼如下圖所示

//方式一更新:測試級聯克隆
public class DeepProtoType implements Serializable, Cloneable {

    public String name; //String類型

    public DeepCloneableTarget deepCloneableTarget; //引用類型

    public DeepProtoType() {
        super();
    }

    //深拷貝 - 方式一:重寫clone方法  級聯

    @Override
    protected Object clone() throws CloneNotSupportedException {

        Object deep = null;

        //這裡完成的是對基本資料類型以及String類型的拷貝
        deep = super.clone();

        //對引用類型的屬性進行單獨處理
        DeepProtoType deepProtoType = (DeepProtoType) deep;

        deepProtoType.deepCloneableTarget= (DeepCloneableTarget) deepCloneableTarget.clone();

        deepProtoType.deepCloneableTarget.witness = (Witness) deepProtoType.deepCloneableTarget.witness.clone();

        return deepProtoType;
    }

    @Override
    public String toString() {
        return "DeepProtoType{" +
                "name='" + name + '\'' +
                ", deepCloneableTarget=" + deepCloneableTarget +
                '}';
    }
}

public class DeepCloneableTarget implements Serializable, Cloneable {

    private static final long serialVersionUID = 1L;

    private String cloneName;

    private String cloneClass;

    public Witness witness; //增加引用類型

    public DeepCloneableTarget(String cloneName, String cloneClass) {
        this.cloneName = cloneName;
        this.cloneClass = cloneClass;
    }

    //因為該類的屬性都是String,是以我們使用預設的clone方法完成即可
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return "DeepCloneableTarget{" +
                "cloneName='" + cloneName + '\'' +
                ", cloneClass='" + cloneClass + '\'' +
                ", witness=" + witness +
                '}';
    }
}

//證明人類
public class Witness implements Serializable,Cloneable {

    private String name;

    private String job; //職務

    public Witness(String name, String job) {
        this.name = name;
        this.job = job;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getJob() {
        return job;
    }

    public void setJob(String job) {
        this.job = job;
    }

    @Override
    public String toString() {
        return "Witness{" +
                "name='" + name + '\'' +
                ", job='" + job + '\'' +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

//方式一更新:測試級聯克隆
public class Client {

    public static void main(String[] args) throws CloneNotSupportedException {

        DeepProtoType deepProtoType = new DeepProtoType();

        deepProtoType.name = "小李";

        deepProtoType.deepCloneableTarget = new DeepCloneableTarget("老李","克隆類");

        deepProtoType.deepCloneableTarget.witness = new Witness("證明人", "職務");

        DeepProtoType clone = (DeepProtoType) deepProtoType.clone();

        DeepProtoType clone1 = (DeepProtoType) deepProtoType.clone();

        System.out.println(clone);

        System.out.println(clone1);

        System.out.println(clone.deepCloneableTarget.witness.hashCode());

        System.out.println(clone1.deepCloneableTarget.witness.hashCode());
        //測試哈希值不一樣  實作了深拷貝

    }
}           

實作方式二:通過對象序列化

上面我們示範了方式一,實作了深拷貝,但同時也帶來了問題,如果有很多個引用類型的成員屬性,或者說(引用類型是個類,裡面仍然有引用類型的成員屬性)這種級聯的情況,需要分步處理,如果要拷貝的對象結構複雜,實作起來非常繁瑣,那有沒有可以一步到位的方法呢?下面我們來示範第二種,使用對象序列化的形式,示例代碼如

下圖所示

public class DeepProtoType implements Serializable, Cloneable {

    public String name; //String類型

    public DeepCloneableTarget deepCloneableTarget; //引用類型

    public DeepProtoType() {
        super();
    }

    //深拷貝 - 方式二:通過對象序列化(推薦的)
    public Object deepClone(){

        //建立流對象
        ByteArrayOutputStream bos = null;

        ObjectOutputStream oos = null;

        ByteArrayInputStream bis = null;

        ObjectInputStream ois = null;

        try {
            //序列化
            bos = new ByteArrayOutputStream();

            oos = new ObjectOutputStream(bos);

            oos.writeObject(this);//将目前對象,以對象流的方式輸出(即序列化)
            //輸出的時候會把包括引用類型一起輸出

            //反序列化
            bis = new ByteArrayInputStream(bos.toByteArray());//把輸出去的對象再讀進來

            ois = new ObjectInputStream(bis);

            DeepProtoType clone = (DeepProtoType)ois.readObject();
            //充分利用序列化和反序列化的特點,把目前對象this以對象流的方式輸出去,然後以對象的方式再讀回來,關聯的引用類型自然也讀回來了

            return clone;

        } catch (Exception e) {

            e.getMessage();

        } finally {

            //關閉流操作
            try {
                bos.close();

                oos.close();

                bis.close();

                ois.close();

            } catch (IOException e) {

                e.getMessage();

            }
        }

        return null;
    }

    @Override
    public String toString() {
        return "DeepProtoType{" +
                "name='" + name + '\'' +
                ", deepCloneableTarget=" + deepCloneableTarget +
                '}';
    }
}

public class DeepCloneableTarget implements Serializable, Cloneable {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    private String cloneName;

    private String cloneClass;

    public DeepCloneableTarget(String cloneName, String cloneClass) {
        this.cloneName = cloneName;
        this.cloneClass = cloneClass;
    }

    //因為該類的屬性都是String,是以我們使用預設的clone方法完成即可
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return "DeepCloneableTarget{" +
                "cloneName='" + cloneName + '\'' +
                ", cloneClass='" + cloneClass + '\'' +
                '}';
    }
}

// 測試 方式二 通過對象序列化
public class Client {

    public static void main(String[] args) throws CloneNotSupportedException {

        DeepProtoType deepProtoType = new DeepProtoType();

        deepProtoType.name = "小李";

        deepProtoType.deepCloneableTarget = new DeepCloneableTarget("老李","克隆類");

        DeepProtoType clone = (DeepProtoType)deepProtoType.deepClone();

        DeepProtoType clone1 = (DeepProtoType)deepProtoType.deepClone();

        System.out.println(clone.toString());

        System.out.println(clone1.toString());

        System.out.println(clone.deepCloneableTarget.hashCode());

        System.out.println(clone1.deepCloneableTarget.hashCode()); //測試哈希值不一緻
    }
}           

可以看到,經測試,哈希值不一樣,說明确實整個對象(包括對象的引用類型)都進行了拷貝 ,我們通過對象序列化的方式也實作了深拷貝,而且利用序列化和反序列化的特點,把目前對象this以對象流的方式輸出去,然後以對象的方式再讀回來,關聯的引用類型的屬性值自然也讀回來了,這種方式是推薦大家使用的,因為是直接把目前整個對象進行序列化和反序列化操作,那不管類的結構如何複雜,都可以通過對象序列化的方式整體處理

注意事項

原型模式到這裡就告一段落了,下面我們一起來總結下原型模式的注意事項,分析下優劣勢以及應用場景

優勢

建立新的對象比較複雜時,可以利用原型模式簡化對象的建立過程,同時也可以提高效率

不用重新初始化對象,而是動态地獲得對象運作時的狀态

如果原始對象發生變化(增加或者減少屬性),其它克隆對象的也會發生相應的變化,無需修改代碼

劣勢

需要為每一個類配備一個克隆方法,這對全新的類來說不是很難,但對已有的類進行改造時,需要修改其源代碼,會違背 ocp 原則,這點需要和小夥伴們提一下

下節預告

OK, 下一節我們進入下一個模式 - 建造者模式的學習,希望大家在學習的過程中,能夠感覺到設計模式的有趣之處,高效而愉快的學習,那我們下期見~

繼續閱讀