23中設計模式之_原型模式(深/淺拷貝)
前言
原型模式其實java Object中已經提供了一個Clone( )方法,平時很少用到,最近讀Retrofit原始碼時候看到有這種使用方式。
定義
原型模式就是在系統clone()標記的基礎上,對Clone()進行復寫,不同的操作可以產生兩種拷貝模式。
UML類圖
原始碼分析
今天我們來講原型模式,這個模式的簡單程度是僅次於單例模式和迭代器模式,非常簡單,但是要使
用好這個模式還有很多注意事項。我們通過一個例子來解釋一下什麼是原型模式。
現在電子賬單越來越流行了,比如你的信用卡,到月初的時候銀行就會發一份電子郵件到你郵箱中,
說你這個月消費了多少,什麼時候消費的,積分是多少等等,這個是每個月發一次,但是還有一種也是銀
行發的郵件你肯定有印象:廣告信,現在各大銀行的信用卡部門都在拉攏客戶,電子郵件是一種廉價、快
捷的通訊方式,你用紙質的廣告信那個費用多高呀,比如我今天推出一個信用卡刷卡抽獎活動,通過電子
賬單系統可以一個晚上傳送給 600 萬客戶,為什麼要用電子賬單系統呢?直接找個發垃圾郵件不就解決問
題了嗎?是個好主意,但是這個方案在金融行業是行不通的,銀行發這種郵件是有要求的,一是一般銀行
都要求個性化服務,發過去的郵件上總有一些個人資訊吧,比如“XX 先生”,“XX 女士”等等,二是郵件的
到達率有一定的要求,由於大批量的傳送郵件會被接收方郵件伺服器誤認是垃圾郵件,因此在郵件頭要增
加一些偽造資料,以規避被反垃圾郵件引擎誤認為是垃圾郵件;從這兩方面考慮廣告信的傳送也是電子賬單系統(電子賬單系統一般包括:賬單分析、賬單生成器、廣告信管理、傳送佇列管理、傳送機、退信處
理、報表管理等)的一個子功能,我們今天就來考慮一下廣告信這個模組是怎麼開發的。那既然是廣告信,
肯定需要一個模版,然後再從資料庫中把客戶的資訊一個一個的取出,放到模版中生成一份完整的郵件,
然後扔給傳送機進行傳送處理,我們來看類圖:
在類圖中 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(); } }
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 多種解決辦法,我們不多
說,我們今天就說一種,使用原型模式來解決這個問題,使用物件的拷貝功能來解決這個問題,類圖稍作
修改,如下圖:
這裡貼出來修改的地方
在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;
}
}
結果就是
[張三]
深拷貝,兩種物件互為獨立,屬於單獨物件
Eg:final 型別修飾的成員變數不能進行深拷貝
原型模式適合在什麼場景使用?一是類初始化需要消化非常多的資源,這個資源包括資料、硬體資源等;二是通過 new 產生一個物件需要非常繁瑣的資料準備或訪問許可權,則可以使用原型模式;三是一個物件需要提供給其他物件訪問,而且各個呼叫者可能都需要修改其值時,可以考慮使用原型模式拷貝多個物件供呼叫者使用。在實際專案中,原型模式很少單獨出現,一般是和工廠方法模式一起出現,通過 clone的方法建立一個物件,然後由工廠方法提供給呼叫者。原型模式先產生出一個包含大量共有資訊的類,然後可以拷貝出副本,修正細節資訊,建立了一個完整的個性物件。