我的Java設計模式-原型模式

Jet啟思發表於2017-10-25

“不好意思,我是臥底!哇哈哈哈~”額......自從寫了上一篇的觀察者模式,就一直沉浸在這個角色當中,無法自撥。昨晚在看《使徒行者2》,有一集說到啊炮仗哥印鈔票,我去,這就是想印多少就印多少的節奏。

但是我覺得他們印鈔票的方法太low了,就用那“哧咔,哧咔~”的老機器沒日沒夜的印,看著都著急。

這裡我們可以用原型模式優化印鈔票的致富之路,為什麼,繼續往下看......

一、原型模式

定義

  用原型例項指定所有建立物件的型別,並且通過複製這個拷貝建立新的物件。

特點

  1)必須存在一個現有的物件,也就是原型例項,通過原型例項建立新物件。

  2)在Java中,實現Cloneable,並且因為所有的類都繼承Object類重寫clone()方法來實現拷貝。

使用場景

  • 大量的物件,並且類初始化時消耗的資源多。沒人會嫌錢多的吧,除了某雲。

  • 這些鈔票的資訊屬性基本一致,可以調整個別的屬性。

  • 印鈔票的工序非常複雜,需要進行繁瑣的資料處理。

UML圖

原型模式UML圖.png
原型模式UML圖.png

從上面的UML圖可以看出,原型模式涉及到的角色有如下三個:

  - 客戶端角色:負責建立物件的請求。

  - 抽象原型角色:該角色是一個抽象類或者是介面,提供拷貝的方法。

  - 具體原型角色:該角色是拷貝的物件,需要重寫抽象原型的拷貝方法,實現淺拷貝或者深拷貝。

二、實戰

一起來印鈔票,鈔票例項必須實現Cloneable介面,該介面只充當一個標記,然後重寫clone方法,具體原型角色程式碼如下:

public class Money implements Cloneable {

    private int faceValue;

    private Area area;

    public int getFaceValue() {
        return faceValue;
    }

    public void setFaceValue(int faceValue) {
        this.faceValue = faceValue;
    }

    public Money(int faceValue, Area area) {
        this.faceValue = faceValue;
        this.area = area;
    }

    public Area getArea() {
        return area;
    }

    public void setArea(Area area) {
        this.area = area;
    }

    public String getUnit() {
        return unit;
    }

    public void setUnit(String unit) {
        this.unit = unit;
    }

    @Override
    protected Money clone() throws CloneNotSupportedException {
        return (Money) super.clone();
    }
}複製程式碼

Area類程式碼如下:

public class Area {

    // 鈔票單位
    private String unit;

    public String getUnit() {
        return unit;
    }

    public void setUnit(String unit) {
        this.unit = unit;
    }

}複製程式碼

看看客戶端如何實現鈔票的拷貝,程式碼如下:

public class Client {

    public static void main(String[] args) {

        Area area = new Area();
        area.setUnit("RMB");

        // 原型例項,100RMB的鈔票
        Money money = new Money(100, area);

        for (int i = 1; i <= 3; i++) {
            try {
                Money cloneMoney = money.clone();
                cloneMoney.setFaceValue(i * 100);
                System.out.println("這張是" + cloneMoney.getFaceValue() +  cloneMoney.getArea().getUnit() + "的鈔票");
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
        }
    }
}複製程式碼

大把大把的鈔票出來了

這張是100RMB的鈔票

這張是200RMB的鈔票

這張是300RMB的鈔票

從上面並沒有看到抽象原型角色的程式碼,那該角色在哪?Object就是這個抽象原型角色,因為Java中所有的類都預設繼承Objet,在這提供clone方法。

三、淺拷貝和深拷貝

在使用原型模式的時候,常常需要注意用的到底是淺拷貝還是深拷貝,當然這必須結合實際的專案需求。下面來了解學習這兩種拷貝的用法和區別:

首先我們來看一個例子,只改變客戶端程式碼:

public class Client {

    public static void main(String[] args) {

        Area area = new Area();
        area.setUnit("RMB");
        // 原型例項,100RMB的鈔票
        Money money = new Money(100, area);
        try {
            Money cloneMoney = money.clone();
            cloneMoney.setFaceValue(200);
            area.setUnit("美元"); 

            System.out.println("原型例項的面值:" + money.getFaceValue() +money.getArea().getUnit());
            System.out.println("拷貝例項的面值:" + cloneMoney.getFaceValue() + cloneMoney.getArea().getUnit());
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }

}複製程式碼

執行結果如下:

原型例項的面值:100美元

拷貝例項的面值:200美元

淺拷貝

見鬼了,明明就把原型例項的單位改成了美元而已,拷貝例項怎麼也會跟著改變的。哪裡有鬼?其實是Java在搞鬼。我們用的是Object的clone方法,而該方法只拷貝按值傳遞的資料,比如String型別和基本型別,但物件內的陣列、引用物件都不拷貝,也就是說記憶體中原型例項和拷貝例項指向同一個引用物件的地址,這就是淺拷貝。淺拷貝的記憶體變化如下圖:

淺拷貝記憶體分析.png
淺拷貝記憶體分析.png

從上圖可以看出,淺拷貝前後的兩個例項物件共同指向同一個記憶體地址,即它們共有擁有area1例項,同時也存在著資料被修改的風險。注意,這裡不可拷貝的引用物件是指可變的類成員變數

深拷貝

同樣的看例子,客戶端程式碼如下:

public class Client {

    public static void main(String[] args) {

        Area area = new Area();
        area.setUnit("RMB");

        // 原型例項,100RMB的鈔票
        Money money = new Money(100, area);

        try {
            Money cloneMoney = money.clone();
            cloneMoney.setFaceValue(200);
            area.setUnit("美元");

            System.out.println("原型例項的面值:" + money.getFaceValue() + money.getArea().getUnit());
            System.out.println("拷貝例項的面值:" + cloneMoney.getFaceValue() + cloneMoney.getArea().getUnit());
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }

}複製程式碼

執行結果如下:

原型例項的面值:100美元

拷貝例項的面值:200RMB

咦~這客戶端程式碼不是跟淺拷貝的一樣嗎,但是執行結果卻又不一樣了。關鍵就在,實現深拷貝就需要完全的拷貝,包括引用物件,陣列的拷貝。所以Area類也實現了Cloneable介面,重寫了clone方法,程式碼如下:

public class Area implements Cloneable{

    // 鈔票單位
    private String unit;

    public String getUnit() {
        return unit;
    }

    public void setUnit(String unit) {
        this.unit = unit;
    }

    @Override
    protected Area clone() throws CloneNotSupportedException {
        Area cloneArea;
        cloneArea = (Area) super.clone();
        return cloneArea;
    }
}複製程式碼

另外,在Money鈔票類的clone方法增加拷貝Area的程式碼:

public class Money implements Cloneable, Serializable {

    private int faceValue;

    private Area area;

    public int getFaceValue() {
        return faceValue;
    }

    public void setFaceValue(int faceValue) {
        this.faceValue = faceValue;
    }

    public Money(int faceValue, Area area) {
        this.faceValue = faceValue;
        this.area = area;
    }

    public Area getArea() {
        return area;
    }

    public void setArea(Area area) {
        this.area = area;
    }

    @Override
    protected Money clone() throws CloneNotSupportedException {
        Money cloneMoney = (Money) super.clone();
        cloneMoney.area = this.area.clone();  // 增加Area的拷貝
        return cloneMoney;
    }

}複製程式碼

深拷貝的記憶體變化如下圖:

深拷貝記憶體分析.png
深拷貝記憶體分析.png

深拷貝除了需要拷貝值傳遞的資料,還需要拷貝引用物件、陣列,即把所有引用的物件都拷貝。需要注意的是拷貝的引用物件是否還有可變的類成員物件,如果有就繼續對該成員物件進行拷貝,如此類推。所以使用深拷貝是注意分析拷貝有多深,以免影響效能。

序列化實現深拷貝

這是實現深拷貝的另一種方式,通過二進位制流操作物件,從而達到深拷貝的效果。把物件寫到流裡的過程是序列化過程,而把物件從流中讀出來的過程則叫反序列化過程。深拷貝的過程就是把物件序列化(寫成二進位制流),然後再反序列化(從流裡讀出來)。注意,在Java中,常常可以先使物件實現Serializable介面,包括引用物件也要實現Serializable介面,不然會拋NotSerializableException。

只要修改Money,程式碼如下:

public class Money implements Serializable {

    private int faceValue;

    private Area area;

    public int getFaceValue() {
        return faceValue;
    }

    public void setFaceValue(int faceValue) {
        this.faceValue = faceValue;
    }

    public Money(int faceValue, Area area) {
        this.faceValue = faceValue;
        this.area = area;
    }

    public Area getArea() {
        return area;
    }

    public void setArea(Area area) {
        this.area = area;
    }

    @Override
    protected Money clone() throws CloneNotSupportedException {
        Money money = null;
        try {
            // 呼叫deepClone,而不是Object的clone方法
            cloneMoney = (Money) deepClone();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return cloneMoney;
    }

    // 通過序列化深拷貝
    public Object deepClone() throws IOException, ClassNotFoundException {
        //將物件寫到流裡
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(this);
        //從流裡讀回來
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        return ois.readObject();
    }
}複製程式碼

同樣執行客戶端程式碼,最後來看看結果:

原型例項的面值:100美元

拷貝例項的面值:200RMB

四、原型模式的優缺點

優點

1)提高效能。不用new物件,消耗的資源少。

缺點

1)淺拷貝時需要實現Cloneable介面,深拷貝則要特別留意是否有引用物件的拷貝。

總結

原型模式本身比較簡單,重寫Object的clone方法,實現淺拷貝還是深拷貝。重點在理解淺拷貝和深拷貝,這是比較細但又重要,卻往往被忽略的知識點。好啦,原型模式就到這了,下一篇是策略模式,敬請關注,拜拜!

設計模式Java原始碼GitHub下載https://github.com/jetLee92/DesignPattern

AndroidJet的開發之路.jpg
AndroidJet的開發之路.jpg

相關文章