天天看點

23中設計模式之_原型模式(深/淺拷貝)

前言

原型模式其實java Object中已經提供了一個Clone( )方法,平時很少用到,最近讀Retrofit源碼時候看到有這種使用方式。

定義

原型模式就是在系統clone()标記的基礎上,對Clone()進行複寫,不同的操作可以産生兩種拷貝模式。

UML類圖

23中設計模式之_原型模式(深/淺拷貝)

源碼分析

今天我們來講原型模式,這個模式的簡單程度是僅次于單例模式和疊代器模式,非常簡單,但是要使

用好這個模式還有很多注意事項。我們通過一個例子來解釋一下什麼是原型模式。

現在電子賬單越來越流行了,比如你的信用卡,到月初的時候銀行就會發一份電子郵件到你郵箱中,

說你這個月消費了多少,什麼時候消費的,積分是多少等等,這個是每個月發一次,但是還有一種也是銀

行發的郵件你肯定有印象:廣告信,現在各大銀行的信用卡部門都在拉攏客戶,電子郵件是一種廉價、快

捷的通訊方式,你用紙質的廣告信那個費用多高呀,比如我今天推出一個信用卡刷卡抽獎活動,通過電子

賬單系統可以一個晚上發送給 600 萬客戶,為什麼要用電子賬單系統呢?直接找個發垃圾郵件不就解決問

題了嗎?是個好主意,但是這個方案在金融行業是行不通的,銀行發這種郵件是有要求的,一是一般銀行

都要求個性化服務,發過去的郵件上總有一些個人資訊吧,比如“XX 先生”,“XX 女士”等等,二是郵件的

到達率有一定的要求,由于大批量的發送郵件會被接收方郵件伺服器誤認是垃圾郵件,是以在郵件頭要增

加一些僞造資料,以規避被反垃圾郵件引擎誤認為是垃圾郵件;從這兩方面考慮廣告信的發送也是電子賬單系統(電子賬單系統一般包括:賬單分析、賬單生成器、廣告信管理、發送隊列管理、發送機、退信處

理、報表管理等)的一個子功能,我們今天就來考慮一下廣告信這個子產品是怎麼開發的。那既然是廣告信,

肯定需要一個模版,然後再從資料庫中把客戶的資訊一個一個的取出,放到模版中生成一份完整的郵件,

然後扔給發送機進行發送處理,我們來看類圖:

23中設計模式之_原型模式(深/淺拷貝)

在類圖中 MailTem是廣告信的模闆,一般都是從資料庫取出,生成一個 BO 或者是 DTO,我們這裡

使用一個靜态的值來做代表;Mail 類是一封郵件類,發送機發送的就是這個類,我們先來看看我們的程式:

貼代碼:

Mail 就是一個業務對象,我們再來看業務場景類是怎麼調用的:

package com.weichao.prototy;
    
    import java.util.Random;
    
    /**
     * 原型模式 銀行電子廣告
     * 
     * @author weichyang
     * 
     *         1.有什麼弊端 2.單線程,發送 600萬封需要多長時間 3.改用多線程
     * 
     */
    public class CopyOfClient {
    
        public static int MAX_COUNT = 5;
    
        public static void main(String[] args) {
            /* 發送郵件 */
            final Mail mail = new Mail(new MailTemp());
            mail.setTail("xxx銀行的所有版權");
    
            for (int i = 0; i < MAX_COUNT; i++) {
                mail.setSub(getRandString(5) + " 先生(女士) ");
                mail.setReceiver(getRandString(5) + "@" + getRandString(8) + ".com");
                sendMail(mail);
    
            }
        }
    
        public static void sendMail(Mail mail) {
            System.out.println("标題: " + mail.getSub() + "\t收件人"
                    + mail.getReceiver() + "\t....發送成功! ");
        }
    
        public static String getRandString(int maxLength) {
            String source = "abcdefghijklmnopqrskuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
            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();
        }
    
    }      

Mail

package com.weichao.prototy;
    
    public class Mail  {
    
        public String receiver;// 接收者
        public String tail;// 結尾備注
        private String context; // 内容
        private String sub; // 标題
    
        public Mail(MailTemp mTemp) {
            this.context = mTemp.getMainContentString();
            this.sub = mTemp.getSubString();
        }
    
        public String getReceiver() {
            return receiver;
        }
    
        public void setReceiver(String receiver) {
            this.receiver = receiver;
        }
    
        public String getTail() {
            return tail;
        }
    
        public void setTail(String tail) {
            this.tail = tail;
        }
    
        public String getContext() {
            return context;
        }
    
        public void setContext(String context) {
            this.context = context;
        }
    
        public String getSub() {
            return sub;
        }
    
        public void setSub(String sub) {
            this.sub = sub;
        }
    
    
    }      

MailTemp

package com.weichao.prototy;
    
    public class MailTemp {
    
        public String subString;// 标題
        public String mainContentString; // 廣告内容
    
        public String getSubString() {
            return "xxxxxxxxxxxxx賬單";
        }
    
        public String getMainContentString() {
            return "xxx" + "(先生/女士)";
        }
    
    }      

運作結果:

标題: OcqZc 先生(女士) 收件人[email protected] …發送成功!

标題: qunOc 先生(女士) 收件人[email protected] …發送成功!

标題: arBDA 先生(女士) 收件人[email protected] …發送成功!

标題: VgaMg 先生(女士) 收件人[email protected] …發送成功!

标題: TxuHD 先生(女士) 收件人[email protected] …發送成功!

由于是随機數,每次運作都由所差異,不管怎麼樣,我們這個電子賬單發送程式時寫出來了,也能發

送出來了,我們再來仔細的想想,這個程式是否有問題?你看,你這是一個線程在運作,也就是你發送是

單線程的, 那按照一封郵件發出去需要 0.02 秒 (夠小了,你還要到資料庫中取資料呢), 600 萬封郵件需要…

我算算(掰指頭計算中…),恩,是 33 個小時,也就是一個整天都發送不完畢,今天發送不完畢,明天的

賬單又産生了,積累積累,激起甲方人員一堆抱怨,那怎麼辦?

好辦,把 sendMail 修改為多線程,但是你隻把 sendMail 修改為多線程還是有問題的呀,你看哦,産

生第一封郵件對象,放到線程 1 中運作,還沒有發送出去;線程 2 呢也也啟動了,直接就把郵件對象 mail的收件人位址和稱謂修改掉了,線程不安全了,好了,說到這裡,你會說這有 N 多種解決辦法,我們不多

說,我們今天就說一種,使用原型模式來解決這個問題,使用對象的拷貝功能來解決這個問題,類圖稍作

修改,如下圖:

23中設計模式之_原型模式(深/淺拷貝)

這裡貼出來修改的地方

在Mail中實作

public class Mail implements Cloneable {
    ...
    ...
    ...
        // 進行淺拷貝
        @Override
        protected Mail clone() throws CloneNotSupportedException {
            Mail mail = (Mail) super.clone();
            return mail;
        }
    
    }      

Client調用的地方

public class Client {
    
        public static int MAX_COUNT = 5;
    
        public static void main(String[] args) {
            /* 發送郵件 */
            final Mail mail = new Mail(new MailTemp());
            mail.setTail("xxx銀行的所有版權");
    
            for (int i = 0; i < MAX_COUNT; i++) {
                Mail cloneMail;
                try {
                    cloneMail = mail.clone();
                    cloneMail.setSub(getRandString(5) + " 先生(女士) ");
                    cloneMail.setReceiver(getRandString(5) + "@" + getRandString(8)
                            + ".com");
                    sendMail(cloneMail);
                } catch (CloneNotSupportedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }
    
    }      

運作結果不變,一樣完成了電子廣告信的發送功能,而且 sendMail 即使是多線程也沒有關系,看到mail.clone()這個方法了嗎?把對象拷貝一份,産生一個新的對象,和原有對象一樣,然後再修改細節的資料,如設定稱謂,設定收件人位址等等。這種不通過 new 關鍵字來産生一個對象,而是通過對象拷貝來實作的模式就叫做原型模式,這個模式的核心是一個clone( )方法,通過這個方法進行對象的拷貝,Java 提供了一個 Cloneable 接口

來标示這個對象是可拷貝的,為什麼說是“标示”呢?翻開 JDK 的幫助看看 Cloneable 是一個方法都沒有

的,這個接口隻是一個标記作用,在 JVM 中具有這個标記的對象才有可能被拷貝,那怎麼才能從“有可能被拷貝”轉換為“可以被拷貝”呢?方法是覆寫clone()方法,是的,你沒有看錯是重寫 clone()方法,看看我們上面 Mail 類:

@Override
    public Mail clone(){}      

在 clone()方法上增加了一個注解@Override, 沒有繼承一個類為什麼可以重寫呢?在 Java 中所有類的

老祖宗是誰?對嘛,Object 類,每個類預設都是繼承了這個類,是以這個用上@Override是非常正确的。原型模式雖然很簡單,但是在 Java 中使用原型模式也就是 clone 方法還是有一些注意事項的,我們通過幾個例子一個一個解說(如果你對 Java 不是很感冒的話,可以跳開以下部分)。

對象拷貝時,類的構造函數是不會被執行的。 一個實作了 Cloneable 并重寫了 clone 方法的類 A,有一個無參構造或有參構造 B,通過 new 關鍵字産生了一個對象 S,再然後通過 S.clone()方式産生了一個新的

對象 T,那麼在對象拷貝時構造函數 B 是不會被執行的,

對象拷貝時确實構造函數沒有被執行,這個從原理來講也是可以講得通的,Object 類的 clone 方法的

原理是從記憶體中(具體的說就是堆記憶體)以二進制流的方式進行拷貝,重新配置設定一個記憶體塊,那構造函數

沒有被執行也是非常正常的了。

淺拷貝和深拷貝問題。 再解釋什麼是淺拷貝什麼是深拷貝前,我們先來看個例子:

package ShallowCopy;
    import java.util.ArrayList;
    
    /**
     * 1.淺拷貝拷貝外層對象,對象裡面的引用對象不進行拷貝。
     * 2.深拷貝需要進行内部的拷貝(人為進行拷貝)。
     * @author weichyang
     * 
     */
    public class ShallowOne implements Cloneable {
    
        public ArrayList<String> getShallowCopyArrayList() {
            return shallowCopyArrayList;
        }
    
        public void setShallowCopyArrayList(ArrayList<String> shallowCopyArrayList) {
            this.shallowCopyArrayList = shallowCopyArrayList;
        }
    
        ArrayList<String> shallowCopyArrayList = new ArrayList<String>();
    
        @SuppressWarnings("unchecked")
        @Override
        protected ShallowOne clone() throws CloneNotSupportedException {
    
            //隻是clone()屬于淺拷貝
            ShallowOne shallowOne = (ShallowOne)     super.clone();
            return shallowOne;
        }
    
    }      

調用

package ShallowCopy;
    
    import java.util.ArrayList;
    
    /**
     * 拷貝 原來 list 進行操作,原來的list中元素同樣會增加 1.前拷貝 隻拷貝基礎資料類型 2.深拷貝,拷貝所有,需要手動進行操作
     * 
     * @author weichyang
     * 
     */
    
    public class Client {
    
        public static void main(String[] args) {
    
            ShallowOne shallowOne = new ShallowOne();
    
            ArrayList<String> strings = shallowOne.getShallowCopyArrayList();
    
            strings.add("張三");
            ArrayList<String> cloneObject = (ArrayList<String>) strings.clone();
    
            cloneObject.add("李四");
    
            System.out.println(cloneObject.toString());
        }
    }      

大家猜想一下運作結果應該是什麼?是就一個“張三”嗎?運作結果如下:

[張三, 李四]

怎麼會有李四呢?是因為 Java 做了一個偷懶的拷貝動作,Object 類提供的方法 clone 隻是拷貝本對象,

其對象内部的數組、引用對象等都不拷貝,還是指向原生對象的内部元素位址,這種拷貝就叫做淺拷貝,

确實是非常淺,兩個對象共享了一個私有變量,你改我改大家都能改,是一個種非常不安全的方式,在實

際項目中使用還是比較少的。你可能會比較奇怪,為什麼在 Mail 那個類中就可以使用使用 String 類型,

而不會産生由淺拷貝帶來的問題呢?内部的數組和引用對象才不拷貝,其他的原始類型比如int,long,String(Java 就希望你把 String 認為是基本類型,String 是沒有 clone 方法的)等都會被拷貝的。

淺拷貝是有風險的,那怎麼才能深入的拷貝呢?我們修改一下我們的程式

public class ShallowOne implements Cloneable {
    
    
        protected ShallowOne clone() throws CloneNotSupportedException {
    
            //隻是clone()屬于淺拷貝
            ShallowOne shallowOne = (ShallowOne) super.clone();
            
            //手動操作屬于深拷貝
             this.shallowCopyArrayList = (ArrayList<String>)
             this.shallowCopyArrayList
             .clone();
    
            return shallowOne;
        }
    
    }      

結果就是

[張三]

深拷貝,兩種對象互為獨立,屬于單獨對象