1. 程式人生 > >設計模式(5)—— 建立型 —— 原型(Prototype)

設計模式(5)—— 建立型 —— 原型(Prototype)

導航

介紹原型模式的基本特點,物件拷貝的運用 。要理解 淺度拷貝深度拷貝 的區別和使用。

原型設計模式介紹

  • 定義:指原型例項指定建立物件的種類,並且通過拷貝這些原型建立新的物件
  • 特點:不需要知道任何建立細節,不呼叫建構函式
  • 型別:建立型
  • 適用場景:
    • 類初始化消耗較多資源
    • new產生的一個物件需要非常頻繁的過程,例如資料準備,訪問許可權等。
    • 建構函式較為複雜
    • 迴圈體中生產大量物件時
  • 優點
    • 效能比直接new一個物件效能高
    • 簡化建立過程
  • 缺點
    • 必須配備克隆方法,克隆方法是這個模式的核心。(Java提供cloneable介面表示物件是可拷貝的;必須重寫Object的clone方法)
    • 對克隆複雜物件或克隆出物件進行復雜改造時,容易引入風險。
    • 深拷貝,淺拷貝要運用得恰當。

一句話來說,就是實現類的克隆,其中包含深度拷貝,淺度拷貝。

程式碼實現

實現程式碼的業務場景:及時通訊app,某個人在特定時間內傳送很多訊息給好友。

首先,定義簡單訊息類,以及傳送訊息的工具類

/**
 * 定義訊息類。
 * 注意點:implement Cloneable ; @Override clone
 */
public class Message implements Cloneable { private String content; private String senderName; private String receiverName; public String getContent() { return content; } public void setContent(String content) { this.content = content; } public String getSenderName
() { return senderName; } public void setSenderName(String senderName) { this.senderName = senderName; } public String getReceiverName() { return receiverName; } public void setReceiverName(String receiverName) { this.receiverName = receiverName; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }

/**
 * 定義傳送訊息類,簡單的print一下。以代表相關的傳送邏輯
 */
public class MessageUtil {

    public static void sendMessage(Message msg){
        System.out.println( msg.getSenderName() +
                            " is Sending a message : {" +
                            msg.getContent() +
                            "} to " +
                            msg.getReceiverName() );
    }

}

下面是測試類:

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

        Message msg = new Message();
        msg.setSenderName("Me");
        msg.setContent("預設內容");
        msg.setReceiverName("預設接收者");

        // 模擬業務場景,此時一個人在某個時間點需要傳送多條訊息。
        for( int i= 0 ; i < 5 ; i ++ ){
            Message clonedMsg =  (Message) msg.clone();
            clonedMsg.setContent("第" + (i+1) + "條訊息");
            clonedMsg.setReceiverName("第" + (i+1) + "個人");
            MessageUtil.sendMessage(clonedMsg);
        }

    }
}

從測試程式碼容易看出,業務頻繁要建立物件例項。並且這個例項我們已知。那麼我們就利用現成的已知物件來克隆新的物件。

讓克隆抽象化

// 抽象可克隆的類。
public class AbstractCloneableClass implements Cloneable {
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
// 具體的實體類
public class Entity extends AbstractCloneableClass {
    
    private String content;
    private String senderName;
    private String receiverName;

    /**
     * 下面是getter和setter,我們省略。僅僅為了展示克隆抽象化的體現
     */
     
     //由於繼承抽象類的關係,我們在實體類中不再需要重寫clone和實現cloneable介面了
}

程式碼通過註釋很容易理解。

淺度拷貝出現的問題

前面我們的成員變數都為String。其實對於成員變數只有基本資料型別和String型別的類,我們在重寫clone函式時,直接使用super.clone()就足以解決問題。但是當我們的成員變數是一個類的時候,此時的拷貝只能拷貝我們前面所說的幾種常見的資料型別。這就要求我們在重寫clone函式時,進行深度拷貝。具體的程式碼實現如下:

現在對於一個User類,我們有它的基本資訊namebirthday
按照前面的實現思路,實現下面的拷貝:

public class User implements Cloneable {
    //User類,注意到成員變數birthday的型別為Birthday自定義Class型別。
    private String name;
    private BirthDay birthday;

    public User(String name, BirthDay time){
        this.name = name;
        this.birthday = time;
    }

    public String getName() {
        return name;
    }

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

    public BirthDay getBirthday() {
        return birthday;
    }

    public void setBirthday(BirthDay birthday) {
        this.birthday = birthday;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
/**
* 生日類。這個類在User類中作為成員變數
*/
public class BirthDay {
    private String year;
    private String month;
    private String day;

    public BirthDay(String year, String month, String day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }

    public String getYear() {
        return year;
    }

    public void setYear(String year) {
        this.year = year;
    }

    public String getMonth() {
        return month;
    }

    public void setMonth(String month) {
        this.month = month;
    }

    public String getDay() {
        return day;
    }

    public void setDay(String day) {
        this.day = day;
    }
}

下面是測試程式碼用例:

public class Test {
    public static void main(String[] args) throws CloneNotSupportedException {
        User user = new User("XiaoMing", new BirthDay( "1998", "1", "1" ) );
        User clonedUser = (User) user.clone();

        /**測試兩個整體User例項,看是否是指向同一地址。執行結果:
             user [email protected]
             cloned user [email protected]
             兩者是否指向同一記憶體地址?false
         */
        System.out.println( "user [email protected]" + user.hashCode() +
                            "\ncloned user [email protected]" + clonedUser.hashCode() +
                            "\n兩者是否指向同一記憶體地址?" + (user==clonedUser) );


        /**
         * 下面是對兩個物件內部的birthday物件進行測試。下面是輸出結果
             User : XiaoMing @1878246837
             Cloned User : XiaoMing @1878246837
             生日是否指向同一記憶體地址:true
         */
        System.out.println( "\nUser : " + user.getName() +
                            " @" + user.getBirthday().hashCode() );

        System.out.println( "Cloned User : " + clonedUser.getName() +
                            " @" + clonedUser.getBirthday().hashCode()  );

        System.out.println( "生日是否指向同一記憶體地址:" + ( user.getBirthday() == clonedUser.getBirthday() ) );


    }

}

從結果我們可以看到,兩個User物件內部的birthday物件的hashCode相等,並且指向同一記憶體地址。這樣的拷貝並不完全。

深度拷貝解決方案

下面是深拷貝的方法:

@Override
    protected Object clone() throws CloneNotSupportedException {

        User clonedUser = (User) super.clone();

        clonedUser.birthday = new BirthDay( clonedUser.getBirthday().getYear(),
                                            clonedUser.getBirthday().getMonth(),
                                            clonedUser.getBirthday().getDay() );
        return clonedUser;
    }

測試用例:

// 用例生成的hashCode的具體值不必在乎。
// 在乎的是比較hashCode的相等性。
public class Test {
    public static void main(String[] args) throws CloneNotSupportedException {
        User user = new User("XiaoMing", new BirthDay( "1998", "1", "1" ) );
        User clonedUser = (User) user.clone();

        /**測試兩個整體User例項,看是否是指向同一地址。執行結果:
             user [email protected]
             cloned user [email protected]
             兩者是否指向同一記憶體地址?false
         */
        System.out.println( "user [email protected]" + user.hashCode() +
                            "\ncloned user [email protected]" + clonedUser.hashCode() +
                            "\n兩者是否指向同一記憶體地址?" + (user==clonedUser) );


        /**
         * 下面是對兩個物件內部的birthday物件進行測試。下面是輸出結果
                 User : XiaoMing @1878246837
                 Cloned User : XiaoMing @929338653
                 生日是否指向同一記憶體地址:false
         */
        System.out.println( "\nUser : " + user.getName() +
                            " @" + user.getBirthday().hashCode() );

        System.out.println( "Cloned User : " + clonedUser.getName() +
                            " @" + clonedUser.getBirthday().hashCode()  );

        System.out.println( "生日是否指向同一記憶體地址:" + ( user.getBirthday() == clonedUser.getBirthday() ) );
    }
}