天天看點

設計模式:原型模式介紹 && 原型模式的深拷貝問題0、背景一、原型模式二、原型模式的淺拷貝、深拷貝問題三、總結

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

如下類圖所示:

設計模式:原型模式介紹 && 原型模式的深拷貝問題0、背景一、原型模式二、原型模式的淺拷貝、深拷貝問題三、總結

其中,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 類裡的成員變量有一個是對象,而不是基礎類型呢?

然後我們建立、再克隆:

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() 方法就是淺拷貝。
設計模式:原型模式介紹 && 原型模式的深拷貝問題0、背景一、原型模式二、原型模式的淺拷貝、深拷貝問題三、總結

在源碼裡也說明了,這個方法是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 原則。