設計模式(四)原型模式

概念

原型模式是指用原型實例指定創建對象的種類,並通過拷貝這些原型來創建新的實例。也就是說原型模式是通過複製現在已存在的對象來創建一個新的對象,被拷貝的對象和新創建的對象類型相同(是同一個類的實例)。使用原型模式時,我們首先要創建一個原型對象,再通過複製這個原型對象來創建更多同類型的對象。原型模式是一種對象創建型模式,創建拷貝對象的工廠是原型類本省,工廠方法由拷貝方法來實現。原型模式的核心是實現拷貝方法。

java中實現拷貝的兩種常用方法

原型模式的核心是實現Clonable接口,重寫clone方法。

通用實現方法

在具體原型類的拷貝方法中實例化一個與自身類型相同的對象,並將其返回,並將相同的對象傳入新創建的對象中,保證他們的成員屬性相同。

public class ConcretePrototype{
    private String userName;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    /**
     * 拷貝方法
     * @return
     */
    public ConcretePrototype clone(){
        ConcretePrototype prototype = new ConcretePrototype();//創建新對象
        prototype.setUserName(this.userName);
        return prototype;
    }
}

class Client{
    public static void main(String[] args){
        ConcretePrototype obj1 = new ConcretePrototype();
        obj1.setUserName("我是小白");
        ConcretePrototype obj2 = obj1.clone();
        System.out.println(obj1.getUserName()+":"+obj2.getUserName());
    }
}

運行結果

我是小白:我是小白

java語言提供的clone()方法

在java語言中所有的java類都繼承自java.lang.Object。而Object類提供了一個clone()方法,可以將一個java對象複製一份。注意:需要實現拷貝的類必須實現Cloneable標識接口,否則當該類調用了clone()方法,編譯器會報異常CloneNotSupportException。

class Client{
    public static void main(String[] args){
        Prototype prototype = new Prototype();
        prototype.setUserName("我是小黑");
        Prototype prototype1 = prototype.clone();
        System.out.println(prototype.getUserName()+":"+prototype1.getUserName());
    }
}

class Prototype implements Cloneable{
    private String userName;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public Prototype clone(){
        Object obj = null;
        try {
            obj = super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return (Prototype)obj;
    }
}
運行結果

我是小黑:我是小黑

解析:java中所有普通類都繼承Object類,既然Object類有clone方法,爲什麼普通了需要實現拷貝的話,還要重寫clone()方法?看Object類源碼就會發現,該方法的定義是:protected native Object clone() throws CloneNotSupportedException;即protected關鍵字修飾的方法,在同一個包下的類才能調用,而普通類和Object不在同一個包下,所以不能直接調用。

如果不實現Cloneable接口,運行該示例,就會報異常

java.lang.CloneNotSupportedException: com.xianjj.patterns.prototype.Prototype
	at java.lang.Object.clone(Native Method)
	at com.xianjj.patterns.prototype.Prototype.clone(ConcretePrototype.java:59)
	at com.xianjj.patterns.prototype.Client.main(ConcretePrototype.java:40)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Exception in thread "main" java.lang.NullPointerException
	at com.xianjj.patterns.prototype.Client.main(ConcretePrototype.java:41)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

淺拷貝與深拷貝

淺拷貝只複製當前對象的變量,不拷貝它所引用的對象,所有的對其他對象的引用仍然指向原來的對象;深拷貝把當前對象和它所引用的對象都拷貝一遍,拷貝前後兩個對象相對獨立互不影響。

淺拷貝示例

public class User implements Cloneable{

    private String userId;
    private String userName;

    @Override
    public User clone(){
        User user = null;
        try {
            user = (User) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return user;
    }
//省略get、set方法。。。
}
public class CloneTest implements Cloneable{
    private User user = new User();
    private int status;

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

    public void getValue() {
        System.out.println("原始類型int:"+status+";引用類型User的屬性userName:"+user.getUserName());
    }

    public void setValue(int status, String userId, String userName) {
        this.status = status;
        this.user.setUserId(userId);
        this.user.setUserName(userName);
    }

}

class CloneClient{
    public static void main(String[] args){
        CloneTest obj1 = new CloneTest();
        obj1.setValue(1, "1", "小黑");

        CloneTest obj2 = obj1.clone();
        obj2.setValue(2, "2", "小白");

        obj1.getValue();
        obj2.getValue();
    }
}
輸出結果

原始類型int:1;引用類型User的屬性userName:小白
原始類型int:2;引用類型User的屬性userName:小白

結果分析:
拷貝後的對象重新賦值時,兩個對象原始類型int類變量status的值不一樣,而應用類型User的值一樣,即拷貝對象obj2中的user值覆蓋了原型對象obj1中user的值。其根本原因是Object類提供的clone()方法只拷貝本對象,對其內部的數組、應用對象都不拷貝,還是指向原生對象的內部元素地址,兩個對象共享了一個私有變量,你改我改大家都能改,這是一種非常不安全的方式。其他的原始類型(int,long,包含String)都會被拷貝。

實現深拷貝

在上述拷貝方法中添加一行代碼,即自行拷貝User對象

@Override
    public CloneTest clone(){
        CloneTest object = null;
        try {
            object = (CloneTest) super.clone();
            object.user = this.user.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return object;
    }
輸出結果:
原始類型int:1;引用類型User的屬性userName:小黑
原始類型int:2;引用類型User的屬性userName:小白

在java中如果要實現深拷貝,可以通過序列化方式(Serialization)來實現。序列化是將對象寫到流的過程,寫到流的對象是原有對象的一個拷貝,然原始對象仍然存與內從中。通過序列化拷貝不僅可以複製對象本身,而且可以複製其應用的成員對象,因此通過序列化將一個對象寫到流中國,再從流裏讀出來可以實現深拷貝。能夠實現序列化的類必須實現Serialization接口。注意:Java中== 比較的是兩個對象的地址

示例展示

示例背景:一些電商爲了維護老用戶,會隔一段時間,通過短信或郵件的方式給用戶發送優惠券或者大額折扣等優惠信息,刺激用戶消費;短信內容基本一致,
只有擡頭信息,收件人不同,假設老用戶有600萬,依次循環發送600萬次,在循環中重新設定短信內容的擡頭信息和收件人,用單線程能實現功能,但是耗時比較長,
如果用多線程,第一個線程沒發送完,新啓的線程會改動發送內容,這樣線程及其不安全;那麼有沒有更好的方式來實現呢?用原型模式。

public class Msg implements Cloneable{
    private String receiver;//收件人
    private String appellation;//稱謂
    private String subject;
    private String context;

    public Msg(MsgTemplete msgTemplete) {
        this.context = msgTemplete.getContext();
        this.subject = msgTemplete.getSubject();
    }

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

    //省略get、set方法
}

class MsgTemplete{
    private String subject="XX店鋪會員送優惠券活動";
    private String context = "老會員只要下單即送價值1萬元的優惠券,可低現金使用。。。";
    public String getSubject(){
        return this.subject;
    }
    public String getContext(){
        return this.context;
    }
}

class MsgClient{
    private static int SEND_MAX = 5;//發送短信的數量

    public static void sendMsg(Msg msg){
        System.out.println("收件人:"+msg.getReceiver()+";短信主題:"+msg.getSubject()+";短信內容:"+msg.getAppellation()+msg.getContext());
    }
    public static void main(String[] args){
        int i = 0;
        //定義模板,可以從數據庫讀取
        Msg msg = new Msg(new MsgTemplete());
        while (i < SEND_MAX){
            //單獨設定每條短信的特殊內容
            Msg msgClone = msg.clone();
            msgClone.setAppellation(getRandString(5)+"先生(女士)");
            msgClone.setReceiver(getRandString(5)+"@"+getRandString(3)+".com");
            //發送短信
            sendMsg(msgClone);
            i++;
        }
    }

    //獲取指定長度的字符串模式收件人
    public static String getRandString(int maxLength){
        String source = "abcdefghijiklmnopqrstuvwrstABCDEFGHIJKLMNOPQRSTUVWXYZ";
        StringBuffer sb = new StringBuffer();
        Random rand = new Random();
        for (int i=0; i<maxLength; i++){
            sb.append(source.charAt(rand.nextInt(source.length())));
        }
        return sb.toString();
    }
}
輸出結果:

收件人:[email protected];短信主題:XX店鋪會員送優惠券活動;短信內容:JVJTZ先生(女士)老會員只要下單即送價值1萬元的優惠券,可低現金使用。。。
收件人:[email protected];短信主題:XX店鋪會員送優惠券活動;短信內容:tRKfg先生(女士)老會員只要下單即送價值1萬元的優惠券,可低現金使用。。。
收件人:[email protected];短信主題:XX店鋪會員送優惠券活動;短信內容:wqCSS先生(女士)老會員只要下單即送價值1萬元的優惠券,可低現金使用。。。
收件人:[email protected];短信主題:XX店鋪會員送優惠券活動;短信內容:OeFNf先生(女士)老會員只要下單即送價值1萬元的優惠券,可低現金使用。。。
收件人:[email protected];短信主題:XX店鋪會員送優惠券活動;短信內容:Wnibn先生(女士)老會員只要下單即送價值1萬元的優惠券,可低現金使用。。。

這裏如果是多線程執行發送短信,也不會出現線程安全問題。

原型模式優缺點

  • 性能優良:原型模式是內存二進制流的拷貝,要不new一個對象性能好很多,特別是要在一個循環體內產生大量的對象時,原型模式可以很好的提現其優點。
  • 逃避構造函數的約束:直接在內存(堆內存)中拷貝構造函數是不會執行的。這既是有點也是缺點。

實際項目中原型模式很少單獨出現,一般是與工廠方法模式一起出現,通過clone的方法創建一個對象,然後由工廠方法提供給調用者。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章