天天看點

設計模式(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類,我們有它的基本資訊name,birthday。

按照前面的實作思路,實作下面的拷貝:

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() ) );
    }
}