設計模式:原型模式介紹 && 原型模式的深拷貝問題

Life_Goes_On發表於2020-08-12

0、背景

克隆羊問題:有一個羊,是一個類,有對應的屬性,要求建立完全一樣的10只羊出來。

那麼實現起來很簡單,我們先寫出羊的類:

public class Sheep {
    private String name;
    private int age;
    private String color;
    //下面寫上對應的get和set方法,以及對應的構造器
}

然後,建立10只一樣的羊,就在客戶端寫一個程式碼建立:

 //原始羊
 Sheep sheep = new Sheep("tom",1,"白色"); 
 //克隆羊 
 Sheep sheep1 = new Sheep(sheep.getName(),sheep.getAge(),sheep.getColor());

sheep1 是克隆的第一隻羊,接著就可以複製十遍這個程式碼,然後命名不同的羊,以原始sheep為模板進行克隆。

這種方法的弊端:

  1. 建立新物件,總是需要重新獲取原始物件的屬性值,效率低;
  2. 總是需要重新初始化物件,而不是動態獲取物件執行時的狀態,不靈活。(什麼意思呢,比如原始的 Sheep 有一項要修改,那麼剩下的以它為範本的,必然要重新初始化)

一、原型模式

  1. 原型模式指的是,用原型例項指定建立物件的種類,並通過拷貝這些原型,建立新的物件;
  2. 原型模式是一種建立型設計模式,允許一個物件再建立另一個可以定製的物件,無需知道如何建立的細節;
  3. 工作原理是:發動建立的這個物件,請求原型物件,讓原型物件來自己實施建立,就是原型物件.clone()

如下類圖所示:

設計模式:原型模式介紹 && 原型模式的深拷貝問題

其中,Prototype 是一個原型介面,在這裡面把克隆自己的方法宣告出來;
ConcreteProtype 可以是一系列的原型類,實現具體操作。

java 的 Object 類是所有類的根類,Object提供了一個 clone() 方法,該方法可以將一個物件複製一份,但是想要實現 clone 的 java 類必須要實現 Cloneable 介面,實現了之後這個類就具有複製的能力。

對於克隆羊問題,我們來利用原型設計模式進行改進:

讓Sheep類,實現 Cloneable 介面:

public class Sheep implements Cloneable{
    private String name;
    private int age;
    private String color;

    //getters&&setters&&constructors
    
    @Override
    protected Object clone() {
        Sheep sheep = null;
        try {
            sheep = (Sheep)super.clone();//使用預設Object的clone方法來完成
        } catch (CloneNotSupportedException e) {
            System.out.println(e.getMessage());
        }
        return sheep;
    }
}

現在的 Sheep 類就是一個具體的原型實現類了,我們想要克隆的時候,客戶端呼叫可以這樣:

Sheep sheep1 = (Sheep) sheep.clone();
Sheep sheep2 = (Sheep) sheep.clone();
//。。。。。類似

這種做法就是原型設計模式。

(spring框架裡,通過bean標籤配置類的scope為prototype,就是用的原型模式)

二、原型模式的淺拷貝、深拷貝問題

使用上面所說的原型模式,按理說是複製出了一模一樣的物件。

但我們做一個嘗試,如果 sheep 類裡的成員變數有一個是物件,而不是基礎型別呢

private Sheep friend;

然後我們建立、再克隆:

Sheep sheep = new Sheep("tom",1,"白色");//原始羊
sheep.setFriend(new Sheep("jack",2,"黑色"));
Sheep sheep1 = (Sheep) sheep.clone();
Sheep sheep2 = (Sheep) sheep.clone();
Sheep sheep3 = (Sheep) sheep.clone();

重寫一下 Sheep 類的 toString 方法,輸出資訊和對應的屬性的 hashcode 後會發現:

Sheep{name='tom', age=1, color='白色', friend=488970385}
Sheep{name='tom', age=1, color='白色', friend=488970385}
Sheep{name='tom', age=1, color='白色', friend=488970385}

friend 的 hashCode 值都一樣,也就是克隆的類的 friend 屬性其實沒有被複制,而是指向了同一個物件。

這就叫淺拷貝(shallow copy):

  1. 對於資料型別是基本資料型別的成員變數,淺拷貝會直接進行值傳遞,也就是複製一份給新物件;
  2. 對於資料型別是引用資料型別的成員變數,淺拷貝會進行引用傳遞,也就是隻是將地址指標複製一份給新物件,實際上覆制前和複製後的內容都指向同一個例項。這種情況,顯然在一個物件裡修改成員變數,會影響到另一個物件的成員變數值(因為修改的都是同一個)
  3. 預設的 clone() 方法就是淺拷貝。
設計模式:原型模式介紹 && 原型模式的深拷貝問題

在原始碼裡也說明了,這個方法是shallow copy 而不是 deep copy

在實際開發中,往往是希望克隆的過程中,如果類的成員是引用型別,也能完全克隆一份,也就是所謂的深拷貝

深拷貝(Deep Copy):

  1. 複製物件的所有基本資料型別成員變數值;
  2. 為所有 引用資料型別 的成員變數申請儲存空間,並且也複製每個 引用資料型別的成員變數 引用的 所有物件,一直到該物件可達的所有物件;

深拷貝的實現方式,需要通過重寫 clone 方法,或者通過物件的序列化。

下面來實現一下。

2.1 通過重寫 clone 方法深拷貝

/*
    被拷貝的類引用的類,此類的clone用預設的clone即可
*/
public class CloneTarget implements Cloneable {
    private static final long serialVersionUID = 1L;
    private String cloneName;
    private String cloneClass;

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

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
/*
    原型類,其中有成員是引用型別,因此clone方法要重寫達到深拷貝
*/
public class Prototype implements Cloneable {
    public String name;
    public CloneTarget cloneTarget;
    public Prototype() {
        super();
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Object o = null;
        //用了淺拷貝,基本資料克隆完成,但是cloneTarget指向的還是原來的物件
        o = super.clone();
        //單獨處理引用型別
        Prototype target = (Prototype) o;
        target.cloneTarget = (CloneTarget)cloneTarget.clone();
        return target;
    }
}

這樣的話,新建一個原型Prototype的物件後,對他進行克隆,得到的裡面的 CloneTarget 成員也是深拷貝的兩個不一樣的物件了。

但是這種方法本質上是相當於 套娃 ,因為都要單獨處理重寫 clone 方法,所以有些麻煩。

2.2 通過物件的序列化

在 Prototype 裡直接 使用序列化+反序列化,達到對這個物件整體的一個複製。

另外注意,序列化和反序列化,必須實現 Serializable 介面,所以 implements 後面不止要有 Cloneable,還有Serializable。

//利用序列化實現深拷貝
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);
        Prototype copy = (Prototype) ois.readObject();
        return copy;
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    }finally {
        try {
            bos.close();
            oos.close();
            bis.close();
            ois.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return null;
}

然後我們想要克隆的時候,直接呼叫這個 deepClone 方法就可以達到目的。

忽視掉裡面的 try - catch 之類的程式碼,其實核心部分就是用到序列化和反序列化的總共 4 個物件。這種方法是推薦的,因為實現起來更加容易。

序列化反序列化達到深拷貝目的的原理:

  • ObjectOutputStream 將 Java 物件的基本資料型別和圖形寫入 OutputStream,但是隻能將支援 java.io.Serializable 介面的物件寫入流中。

在這裡,我們採用的OutputStream是ByteArrayOutputStream——位元組陣列輸出流,通過建立的ObjectOutputStream的writeObject方法,把物件寫進了這個位元組陣列輸出流。

  • 相對應的,ObjectInputStream反序列化原始資料,恢復以前序列化的那些物件。

在這裡,把位元組陣列重新構造成一個ByteArrayInputStream——位元組陣列輸入流,通過ObjectInputStream的readObject方法,把輸入流重新構造成一個物件。

結合上面的程式碼再看看:

bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);//寫入指定的OutputStream
oos.writeObject(this);//把物件寫入到輸出流中,整個物件,this

bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);//讀取指定的InputStream
Prototype copy = (Prototype) ois.readObject();//從輸入流中讀取一個物件

return copy;

三、總結

原型模式:

  1. 當需要建立一個新的物件的內容比較複雜的時候,可以利用原型模式來簡化建立的過程,同時能夠提高效率。
  2. 因為這樣不用重新初始化物件,而是動態地獲得物件執行時的狀態,如果原始的物件內部發生變化,其他克隆物件也會發生相應變化,無需一 一修改。
  3. 實現深拷貝的方法要注意。

缺點:

每一個類都需要一個克隆方法,對於全新的類來說不是問題,但是如果是用已有的類進行改造,那麼可能會因為要修改原始碼而違背 OCP 原則。

相關文章