設計模式之原型模式

0x01.定義與類型

  • 定義:指原型實例指定建立對象的種類,而且經過拷貝這些原型建立新的對象
  • 特色:不須要知道任何建立的細節,不調用構造函數
  • 類型:建立型
  • UML

    原型模式

  • 原型模式主要用於對象的複製,它的核心是就是類圖中的原型類Prototype。Prototype類須要具有如下兩個條件:html

    1. 實現Cloneable接口。在java語言有一個Cloneable接口,它的做用只有一個,就是在運行時通知虛擬機能夠安全地在實現了此接口的類上使用clone方法。在java虛擬機中,只有實現了這個接口的類才能夠被拷貝,不然在運行時會拋出CloneNotSupportedException異常。
    2. 重寫Object類中的clone方法。Java中,全部類的父類都是Object類,Object類中有一個clone方法,做用是返回對象的一個拷貝,可是其做用域protected類型的,通常的類沒法調用,所以Prototype類須要將clone方法的做用域修改成public類型。
  • Java實現
/**
 * 原型模式
 */
public class Prototype implements Cloneable {

    private String name;

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public String getName() {
        return name;
    }

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

/**
 * 測試與應用類
 */
public class Test {

    public static void main(String[] args) throws CloneNotSupportedException {
        Prototype prototype = new Prototype();
        prototype.setName("K.O");
        List<String> names = new ArrayList<>();
        names.add("K.O");
        prototype.setNames(names);
        for (int i = 0; i < 5; i ++) {
            Prototype p = (Prototype) prototype.clone();
            p.setName("sigma");
            p.getNames().add("sigma");
            System.out.println(p.toString());
            System.out.println(p.getName());
            System.out.println(p.getNames().size());

        }
        System.out.println(prototype.toString());
        System.out.println(prototype.getName());
        System.out.println(prototype.getNames().size());
    }
}
  • 測試輸出結果
org.ko.prototype.basic.Prototype@1540e19d
sigma
2
org.ko.prototype.basic.Prototype@677327b6
sigma
3
org.ko.prototype.basic.Prototype@14ae5a5
sigma
4
org.ko.prototype.basic.Prototype@7f31245a
sigma
5
org.ko.prototype.basic.Prototype@6d6f6e28
sigma
6
org.ko.prototype.basic.Prototype@135fbaa4
K.O
6
  • 能夠看出,輸出結果中對象的地址不一樣(是從新建立的)java

    • 修改基本類型時,並不能影響基礎類,而引用對象只是指向的基礎類的屬性。
    • 這裏有個問題叫深拷貝,淺拷貝,後續會介紹!
  • 原型模式的各個元素,原型模式比較簡單,元素比較少git

    • 原型接口:適用原型模式要實現原型接口,重寫裏面的 clone()方法
    • 原型類:具體產品的實現

0x02.使用場景

  • 類初始化消耗較多資源。
  • new產生的一個對象須要很是繁瑣的過程(數據準備、訪問權限等)。
  • 構造函數比較複雜。
  • 循環體中生產大量對象時。

0x03.優勢

  • 原型模式性能比直接new一個對象性能高,是在內存中二進制流的拷貝,要比直接new一個對象性能好不少,特別是要在一個循環體內產生大量對象時,原型模式可能更好的體現其優勢。
  • 還能夠簡化建立過程。
  • 還有一個重要的用途就是保護性拷貝,也就是對某個對象對外多是隻讀的,爲了防止外部對這個只讀對象的修改,一般能夠經過返回一個對象拷貝的形式實現只讀的限制。

0x04.缺點

  • 首先要記住原型模式的拷貝時不會執行構造函數的。
  • clone並不必定比new一個對象快,只有當new對象比較耗時時,才考慮使用原型模式。
  • 必須配備克隆方法。
  • 對克隆複雜對象或對克隆出的對象進行復雜改造時,容易引入風險。
  • 深拷貝、淺拷貝要運用得當。
  • 要使用clone方法,類的成員變量上不要增長final關鍵字,final類型是不容許重賦值的。

0x05.樣例實現

  • 使用原型模式實現發送郵件
  • Java代碼實現
/**
 * Mail實現類
 * 實現 Cloneable 接口
 * 重寫 Object.clone() 方法
 */
public class Mail implements Cloneable {

    private String name;

    private String emailAddress;

    private String content;

    public Mail () {
        System.out.println("Mail Class Constructor!");
    }

    public String getName() {
        return name;
    }

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

    public String getEmailAddress() {
        return emailAddress;
    }

    public void setEmailAddress(String emailAddress) {
        this.emailAddress = emailAddress;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    @Override
    public String toString() {
        return "Mail{" +
                "name='" + name + '\'' +
                ", emailAddress='" + emailAddress + '\'' +
                ", content='" + content + '\'' +
                '}' + super.toString();
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        System.out.println("clone mail object!");
        return super.clone();
    }
}

/**
 * Mail工具類
 */
public class MailUtil {

    public static void sendMail (Mail mail) {
        String outputContent = "向{0}同窗, 郵件地址:{1},郵件內容:{2}, 發送郵件成功!";
        System.out.println(MessageFormat.format(
                outputContent,
                mail.getName(),
                mail.getEmailAddress(), mail.getContent())
        );
    }

    public static void saveOriginMailRecord (Mail mail) {
        System.out.println("存儲originMail記錄, originMail: " + mail.getContent());
    }
}
  • 測試與應用
/**
 * 測試與應用
 */
public class Test {

    public static void main(String[] args) throws CloneNotSupportedException {
        Mail mail = new Mail();
        mail.setContent("初始化模板");

        System.out.println("初始化mail: " + mail.toString());
        for (int i = 0; i < 10; i++) {
            Mail mailTemp = (Mail) mail.clone(); //並無調用Mail構造器
            mailTemp.setName("K.O_" + i);
            mailTemp.setEmailAddress("ko.shen_" + i + "@hotmail.com");
            mailTemp.setContent("恭喜您中獎了。");
            MailUtil.sendMail(mailTemp);
            System.out.println("克隆的mailTemp: " + mailTemp.toString());
        }
        MailUtil.saveOriginMailRecord(mail);
    }
}
  • 測試結果
Mail Class Constructor!
初始化mail: Mail{name='null', emailAddress='null', content='初始化模板'}org.ko.prototype.v2.Mail@1540e19d
clone mail object!
向K.O_0同窗, 郵件地址:ko.shen_0@hotmail.com,郵件內容:恭喜您中獎了。, 發送郵件成功!
克隆的mailTemp: Mail{name='K.O_0', emailAddress='ko.shen_0@hotmail.com', content='恭喜您中獎了。'}org.ko.prototype.v2.Mail@677327b6
clone mail object!
向K.O_1同窗, 郵件地址:ko.shen_1@hotmail.com,郵件內容:恭喜您中獎了。, 發送郵件成功!
克隆的mailTemp: Mail{name='K.O_1', emailAddress='ko.shen_1@hotmail.com', content='恭喜您中獎了。'}org.ko.prototype.v2.Mail@14ae5a5
clone mail object!
向K.O_2同窗, 郵件地址:ko.shen_2@hotmail.com,郵件內容:恭喜您中獎了。, 發送郵件成功!
克隆的mailTemp: Mail{name='K.O_2', emailAddress='ko.shen_2@hotmail.com', content='恭喜您中獎了。'}org.ko.prototype.v2.Mail@7f31245a
clone mail object!
向K.O_3同窗, 郵件地址:ko.shen_3@hotmail.com,郵件內容:恭喜您中獎了。, 發送郵件成功!
克隆的mailTemp: Mail{name='K.O_3', emailAddress='ko.shen_3@hotmail.com', content='恭喜您中獎了。'}org.ko.prototype.v2.Mail@6d6f6e28
clone mail object!
向K.O_4同窗, 郵件地址:ko.shen_4@hotmail.com,郵件內容:恭喜您中獎了。, 發送郵件成功!
克隆的mailTemp: Mail{name='K.O_4', emailAddress='ko.shen_4@hotmail.com', content='恭喜您中獎了。'}org.ko.prototype.v2.Mail@135fbaa4
clone mail object!
向K.O_5同窗, 郵件地址:ko.shen_5@hotmail.com,郵件內容:恭喜您中獎了。, 發送郵件成功!
克隆的mailTemp: Mail{name='K.O_5', emailAddress='ko.shen_5@hotmail.com', content='恭喜您中獎了。'}org.ko.prototype.v2.Mail@45ee12a7
clone mail object!
向K.O_6同窗, 郵件地址:ko.shen_6@hotmail.com,郵件內容:恭喜您中獎了。, 發送郵件成功!
克隆的mailTemp: Mail{name='K.O_6', emailAddress='ko.shen_6@hotmail.com', content='恭喜您中獎了。'}org.ko.prototype.v2.Mail@330bedb4
clone mail object!
向K.O_7同窗, 郵件地址:ko.shen_7@hotmail.com,郵件內容:恭喜您中獎了。, 發送郵件成功!
克隆的mailTemp: Mail{name='K.O_7', emailAddress='ko.shen_7@hotmail.com', content='恭喜您中獎了。'}org.ko.prototype.v2.Mail@2503dbd3
clone mail object!
向K.O_8同窗, 郵件地址:ko.shen_8@hotmail.com,郵件內容:恭喜您中獎了。, 發送郵件成功!
克隆的mailTemp: Mail{name='K.O_8', emailAddress='ko.shen_8@hotmail.com', content='恭喜您中獎了。'}org.ko.prototype.v2.Mail@4b67cf4d
clone mail object!
向K.O_9同窗, 郵件地址:ko.shen_9@hotmail.com,郵件內容:恭喜您中獎了。, 發送郵件成功!
克隆的mailTemp: Mail{name='K.O_9', emailAddress='ko.shen_9@hotmail.com', content='恭喜您中獎了。'}org.ko.prototype.v2.Mail@7ea987ac
存儲originMail記錄, originMail: 初始化模板
  • 從輸出信息能夠看出來,使用clone方法不須要經過構造函數建立
  • 因爲原型模式uml比較簡單,和上面基本一致,這裏就再也不介紹了!

0x06.擴展(深拷貝與淺拷貝)

1. 淺克隆

  • 直接實現原型模式
/**
 * 淺克隆
 */
public class Pig1 implements Cloneable {
    private String name;

    private Date birthday;

    public Pig1(String name, Date birthday) {
        this.name = name;
        this.birthday = birthday;
    }

    public String getName() {
        return name;
    }

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

    public Date getBirthday() {
        return birthday;
    }

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

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return "Pig1{" +
                "name='" + name + '\'' +
                ", birthday=" + birthday +
                '}' + super.toString();
    }
}
  • 測試類
public class Test1 {

    public static void main(String[] args) throws CloneNotSupportedException {
        //淺克隆, 沒辦法克隆引用對象
        Date birthday = new Date(0l);

        Pig1 pig1 = new Pig1("佩奇", birthday);
        Pig1 pig2 = (Pig1) pig1.clone();

        System.out.println(pig1);
        System.out.println(pig2);
        System.out.println("-------");

        pig1.getBirthday().setTime(666666666666L);

        System.out.println(pig1);
        System.out.println(pig2);
    }
}
  • 輸出日誌
Pig1{name='佩奇', birthday=Thu Jan 01 08:00:00 CST 1970}org.ko.prototype.clone.Pig1@6d6f6e28
Pig1{name='佩奇', birthday=Thu Jan 01 08:00:00 CST 1970}org.ko.prototype.clone.Pig1@135fbaa4
-------
Pig1{name='佩奇', birthday=Sat Feb 16 09:11:06 CST 1991}org.ko.prototype.clone.Pig1@6d6f6e28
Pig1{name='佩奇', birthday=Sat Feb 16 09:11:06 CST 1991}org.ko.prototype.clone.Pig1@135fbaa4
  • 如上所示,修改pig1中的birthday,pig2中的也響應了變化,因此直接使用clone方法返回的對象中的引用變量並無從新建立而是直接複用的原有對象中的變量。
  • 由此得出結論:clone方法默認使用的是淺拷貝。
  • 若是想要引用變量也所有複製?

2.深拷貝

  • 其實深克隆只是本身實現了引用變量的建立,請看實現:
/**
 * 深克隆
 */
public class Pig2 implements Cloneable {
    private String name;

    private Date birthday;

    public Pig2(String name, Date birthday) {
        this.name = name;
        this.birthday = birthday;
    }

    public String getName() {
        return name;
    }

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

    public Date getBirthday() {
        return birthday;
    }

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

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Pig2 pig2 = (Pig2) super.clone();

        //深克隆, 要本身去作對象引用的克隆
        pig2.birthday = (Date) pig2.birthday.clone();
        return pig2;
    }

    @Override
    public String toString() {
        return "Pig1{" +
                "name='" + name + '\'' +
                ", birthday=" + birthday +
                '}' + super.toString();
    }
}
  • 測試類
public class Test2 {

    public static void main(String[] args) throws CloneNotSupportedException {
        //深克隆
        Date birthday = new Date(0l);

        Pig2 p1 = new Pig2("佩奇", birthday);
        Pig2 p2 = (Pig2) p1.clone();

        System.out.println(p1);
        System.out.println(p2);
        System.out.println("-------");

        p1.getBirthday().setTime(666666666666L);

        System.out.println(p1);
        System.out.println(p2);
    }
}
  • 輸出日誌
Pig2{name='佩奇', birthday=Thu Jan 01 08:00:00 CST 1970}org.ko.prototype.clone.Pig2@6d6f6e28
Pig2{name='佩奇', birthday=Thu Jan 01 08:00:00 CST 1970}org.ko.prototype.clone.Pig2@135fbaa4
-------
Pig2{name='佩奇', birthday=Sat Feb 16 09:11:06 CST 1991}org.ko.prototype.clone.Pig2@6d6f6e28
Pig2{name='佩奇', birthday=Thu Jan 01 08:00:00 CST 1970}org.ko.prototype.clone.Pig2@135fbaa4
  • 能夠看出,修改了pig1的時間,pig2並無跟着響應。因此深拷貝完成。

0x07.原型模式對單例模式的破壞

  • 當單例對象實現了clone方法時,會返回多個實例,請看實現:
/**
 * 簡單的餓漢式單例
 */
public class StaticInnerClassSingleton implements Cloneable {

    /**
     * 看靜態類的初始化鎖那個線程能夠拿到
     */
    private static class InnerClass {
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance() {
        return InnerClass.staticInnerClassSingleton;
    }

    private StaticInnerClassSingleton() {
        if (InnerClass.staticInnerClassSingleton != null) {
            throw new RuntimeException("單例對象禁止反射調用");
        }
    }

    /**
     * 直接重寫clone方法
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class Test {

    public static void main(String[] args) throws CloneNotSupportedException {
        //獲取單例對象
        StaticInnerClassSingleton singleton = StaticInnerClassSingleton.getInstance();
        System.out.println(singleton.toString());

        //clone獲取克隆對象
        StaticInnerClassSingleton singleton1 = (StaticInnerClassSingleton) singleton.clone();
        System.out.println(singleton1.toString());
    }
}
  • 輸出日誌
org.ko.prototype.singleton.StaticInnerClassSingleton@1540e19d
org.ko.prototype.singleton.StaticInnerClassSingleton@677327b6
  • 根據日誌能夠看出,單例模式被破壞掉。
  • 重寫clone()方法,直接返回INSTANCE對象解決原型模式對單例模式的破壞
/**
 * 簡單的餓漢式單例
 */
public class StaticInnerClassSingleton1 implements Cloneable {

    /**
     * 看靜態類的初始化鎖那個線程能夠拿到
     */
    private static class InnerClass {
        private static StaticInnerClassSingleton1 staticInnerClassSingleton = new StaticInnerClassSingleton1();
    }

    public static StaticInnerClassSingleton1 getInstance() {
        return InnerClass.staticInnerClassSingleton;
    }

    private StaticInnerClassSingleton1() {
        if (InnerClass.staticInnerClassSingleton != null) {
            throw new RuntimeException("單例對象禁止反射調用");
        }
    }

    /**
     * 修改克隆方法,返回單例對象
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return InnerClass.staticInnerClassSingleton;
    }
}

/**
 * 修改後的測試類
 */
public class Test1 {
    public static void main(String[] args) throws CloneNotSupportedException {
        //獲取單例對象
        StaticInnerClassSingleton1 singleton1 = StaticInnerClassSingleton1.getInstance();
        System.out.println(singleton1.toString());

        //獲取clone對象
        StaticInnerClassSingleton1 singleton2 = (StaticInnerClassSingleton1) singleton1.clone();
        System.out.println(singleton2.toString());
    }
}
  • 輸出日誌
org.ko.prototype.singleton.StaticInnerClassSingleton1@1540e19d
org.ko.prototype.singleton.StaticInnerClassSingleton1@1540e19d
  • 能夠看出,返回的對象地址時一致的。這樣就解決了原型對單例模式的破壞。

0x08.源碼地址

0x09.參考

相關文章
相關標籤/搜索