前言
本文的主要內(nèi)容如下:
- 介紹原型模式
- 示例
- Java語言的clone
- 淺克隆與深克隆
- 實(shí)現(xiàn)深克隆
- 原型模式的典型應(yīng)用
原型模式
原型模式(Prototype Pattern):使用原型實(shí)例指定創(chuàng)建對(duì)象的種類得滤,并且通過拷貝這些原型創(chuàng)建新的對(duì)象陨献。原型模式是一種對(duì)象創(chuàng)建型模式。
原型模式的工作原理很簡單:將一個(gè)原型對(duì)象傳給那個(gè)要發(fā)動(dòng)創(chuàng)建的對(duì)象懂更,這個(gè)要發(fā)動(dòng)創(chuàng)建的對(duì)象通過請(qǐng)求原型對(duì)象拷貝自己來實(shí)現(xiàn)創(chuàng)建過程眨业。
原型模式是一種“另類”的創(chuàng)建型模式急膀,創(chuàng)建克隆對(duì)象的工廠就是原型類自身,工廠方法由克隆方法來實(shí)現(xiàn)龄捡。
需要注意的是通過克隆方法所創(chuàng)建的對(duì)象是全新的對(duì)象卓嫂,它們在內(nèi)存中擁有新的地址,通常對(duì)克隆所產(chǎn)生的對(duì)象進(jìn)行修改對(duì)原型對(duì)象不會(huì)造成任何影響聘殖,每一個(gè)克隆對(duì)象都是相互獨(dú)立的晨雳。通過不同的方式修改可以得到一系列相似但不完全相同的對(duì)象。
角色
- Prototype(抽象原型類):它是聲明克隆方法的接口奸腺,是所有具體原型類的公共父類悍募,可以是抽象類也可以是接口,甚至還可以是具體實(shí)現(xiàn)類洋机。
- ConcretePrototype(具體原型類):它實(shí)現(xiàn)在抽象原型類中聲明的克隆方法坠宴,在克隆方法中返回自己的一個(gè)克隆對(duì)象。
- Client(客戶類):讓一個(gè)原型對(duì)象克隆自身從而創(chuàng)建一個(gè)新的對(duì)象绷旗,在客戶類中只需要直接實(shí)例化或通過工廠方法等方式創(chuàng)建一個(gè)原型對(duì)象喜鼓,再通過調(diào)用該對(duì)象的克隆方法即可得到多個(gè)相同的對(duì)象。由于客戶類針對(duì)抽象原型類Prototype編程衔肢,因此用戶可以根據(jù)需要選擇具體原型類庄岖,系統(tǒng)具有較好的可擴(kuò)展性,增加或更換具體原型類都很方便角骤。
原型模式的核心在于如何實(shí)現(xiàn)克隆方法隅忿。
示例
Java語言提供的clone()方法
學(xué)過Java語言的人都知道,所有的Java類都繼承自 java.lang.Object
邦尊。事實(shí)上背桐,Object
類提供一個(gè) clone()
方法,可以將一個(gè)Java對(duì)象復(fù)制一份蝉揍。因此在Java中可以直接使用 Object
提供的 clone()
方法來實(shí)現(xiàn)對(duì)象的克隆链峭,Java語言中的原型模式實(shí)現(xiàn)很簡單。
需要注意的是能夠?qū)崿F(xiàn)克隆的Java類必須實(shí)現(xiàn)一個(gè) 標(biāo)識(shí)接口 Cloneable
又沾,表示這個(gè)Java類支持被復(fù)制弊仪。如果一個(gè)類沒有實(shí)現(xiàn)這個(gè)接口但是調(diào)用了clone()方法,Java編譯器將拋出一個(gè) CloneNotSupportedException
異常杖刷。
public class Mail implements Cloneable{
private String name;
private String emailAddress;
private String content;
public Mail(){
System.out.println("Mail Class Constructor");
}
// ...省略 getter励饵、setter
@Override
protected Object clone() throws CloneNotSupportedException {
System.out.println("clone mail object");
return super.clone();
}
}
在客戶端創(chuàng)建原型對(duì)象和克隆對(duì)象也很簡單,如下代碼所示:
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Mail mail = new Mail();
mail.setContent("初始化模板");
System.out.println("初始化mail:"+mail);
for(int i = 0;i < 3;i++){
System.out.println();
Mail mailTemp = (Mail) mail.clone();
mailTemp.setName("姓名"+i);
mailTemp.setEmailAddress("姓名"+i+"@test.com");
mailTemp.setContent("恭喜您滑燃,此次抽獎(jiǎng)活動(dòng)中獎(jiǎng)了");
MailUtil.sendMail(mailTemp);
System.out.println("克隆的mailTemp:"+mailTemp);
}
MailUtil.saveOriginMailRecord(mail);
}
}
其中的 MailUtil
工具類為
public class MailUtil {
public static void sendMail(Mail mail) {
String outputContent = "向{0}同學(xué),郵件地址:{1},郵件內(nèi)容:{2}發(fā)送郵件成功";
System.out.println(MessageFormat.format(outputContent, mail.getName(), mail.getEmailAddress(), mail.getContent()));
}
public static void saveOriginMailRecord(Mail mail) {
System.out.println("存儲(chǔ)originMail記錄,originMail:" + mail.getContent());
}
}
輸出如下:
Mail Class Constructor
初始化mail:Mail{name='null', emailAddress='null', content='初始化模板'}com.designpattern.prototype.Mail@12edcd21
clone mail object
向姓名0同學(xué),郵件地址:姓名0@test.com,郵件內(nèi)容:恭喜您役听,此次抽獎(jiǎng)活動(dòng)中獎(jiǎng)了發(fā)送郵件成功
克隆的mailTemp:Mail{name='姓名0', emailAddress='姓名0@test.com', content='恭喜您,此次抽獎(jiǎng)活動(dòng)中獎(jiǎng)了'}com.designpattern.prototype.Mail@34c45dca
clone mail object
向姓名1同學(xué),郵件地址:姓名1@test.com,郵件內(nèi)容:恭喜您,此次抽獎(jiǎng)活動(dòng)中獎(jiǎng)了發(fā)送郵件成功
克隆的mailTemp:Mail{name='姓名1', emailAddress='姓名1@test.com', content='恭喜您禾嫉,此次抽獎(jiǎng)活動(dòng)中獎(jiǎng)了'}com.designpattern.prototype.Mail@52cc8049
clone mail object
向姓名2同學(xué),郵件地址:姓名2@test.com,郵件內(nèi)容:恭喜您,此次抽獎(jiǎng)活動(dòng)中獎(jiǎng)了發(fā)送郵件成功
克隆的mailTemp:Mail{name='姓名2', emailAddress='姓名2@test.com', content='恭喜您蚊丐,此次抽獎(jiǎng)活動(dòng)中獎(jiǎng)了'}com.designpattern.prototype.Mail@5b6f7412
存儲(chǔ)originMail記錄,originMail:初始化模板
從輸出結(jié)果中我們可以觀察到:
- for循環(huán)中的 mailTemp 從 mail 對(duì)象中克隆得到熙参,它們的內(nèi)存地址均不同,說明不是同一個(gè)對(duì)象麦备,克隆成功孽椰,克隆僅僅通過調(diào)用
super.clone()
即可。 - 最后調(diào)用的
MailUtil.saveOriginMailRecord(mail);
中的mail
對(duì)象的內(nèi)容仍為 for 循環(huán)之前設(shè)置的內(nèi)容凛篙,并沒有因?yàn)榭寺《淖儭?/li> - 克隆的時(shí)候調(diào)用了
clone
方法黍匾,并沒有調(diào)用Mail
類的構(gòu)造器,只在最前面new
的時(shí)候才調(diào)用了一次
關(guān)于輸出的內(nèi)存地址是怎么輸出的呛梆,我們還需要看一下 Object#toString
方法
public class Object {
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
//...省略...
}
所以所謂的內(nèi)存地址即為 hashCode()
的十六進(jìn)制表示锐涯,這里簡單的認(rèn)為 內(nèi)存地址相同則為同一個(gè)對(duì)象,不同則為不同對(duì)象
再來看一眼 Object#clone
方法
protected native Object clone() throws CloneNotSupportedException;
這是一個(gè) native
關(guān)鍵字修飾的方法
一般而言填物,Java語言中的clone()方法滿足:
- 對(duì)任何對(duì)象x纹腌,都有
x.clone() != x
,即克隆對(duì)象與原型對(duì)象不是同一個(gè)對(duì)象滞磺; - 對(duì)任何對(duì)象x升薯,都有
x.clone().getClass() == x.getClass()
,即克隆對(duì)象與原型對(duì)象的類型一樣击困; - 如果對(duì)象x的
equals()
方法定義恰當(dāng)涎劈,那么x.clone().equals(x)
應(yīng)該成立。
為了獲取對(duì)象的一份拷貝阅茶,我們可以直接利用Object類的clone()方法蛛枚,具體步驟如下:
- 在派生類中覆蓋基類的
clone()
方法,并聲明為public脸哀; - 在派生類的
clone()
方法中坤候,調(diào)用super.clone()
; - 派生類需實(shí)現(xiàn)Cloneable接口企蹭。
此時(shí)白筹,Object類相當(dāng)于抽象原型類,所有實(shí)現(xiàn)了Cloneable接口的類相當(dāng)于具體原型類谅摄。
淺克隆與深克隆
看下面的示例
public class Pig implements Cloneable{
private String name;
private Date birthday;
// ...getter, setter, construct
@Override
protected Object clone() throws CloneNotSupportedException {
Pig pig = (Pig)super.clone();
return pig;
}
@Override
public String toString() {
return "Pig{" +
"name='" + name + '\'' +
", birthday=" + birthday +
'}'+super.toString();
}
}
測試
public class Test {
public static void main(String[] args) throws CloneNotSupportedException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Date birthday = new Date(0L);
Pig pig1 = new Pig("佩奇",birthday);
Pig pig2 = (Pig) pig1.clone();
System.out.println(pig1);
System.out.println(pig2);
pig1.getBirthday().setTime(666666666666L);
System.out.println(pig1);
System.out.println(pig2);
}
}
輸出如下
Pig{name='佩奇', birthday=Thu Jan 01 08:00:00 CST 1970}com.designpattern.clone.Pig@27973e9b
Pig{name='佩奇', birthday=Thu Jan 01 08:00:00 CST 1970}com.designpattern.clone.Pig@312b1dae
Pig{name='佩奇', birthday=Sat Feb 16 09:11:06 CST 1991}com.designpattern.clone.Pig@27973e9b
Pig{name='佩奇', birthday=Sat Feb 16 09:11:06 CST 1991}com.designpattern.clone.Pig@312b1dae
我們照著上一小節(jié)說的實(shí)現(xiàn) Cloneable
徒河,調(diào)用 super.clone();
進(jìn)行克隆,中間我們對(duì) pig1
對(duì)象設(shè)置了一個(gè)時(shí)間戳送漠,從輸出中我們可以發(fā)現(xiàn)什么問題呢顽照?
我們可以發(fā)現(xiàn):
-
pig1
與pig2
的內(nèi)存地址不同 - 對(duì)
pig1
設(shè)置了時(shí)間,同事pig2
的時(shí)間也改變了
我們通過 debug 來看一下
發(fā)現(xiàn)如下:
- pig1 與 pig2 地址不一樣
- pig1 的 birthday 與 pig2 的 birthday 一樣
這里引出淺拷貝與深拷貝。
在Java語言中代兵,數(shù)據(jù)類型分為值類型(基本數(shù)據(jù)類型)和引用類型尼酿,值類型包括int、double植影、byte裳擎、boolean、char等簡單數(shù)據(jù)類型思币,引用類型包括類鹿响、接口、數(shù)組等復(fù)雜類型谷饿。
淺克隆和深克隆的主要區(qū)別在于是否支持引用類型的成員變量的復(fù)制惶我,下面將對(duì)兩者進(jìn)行詳細(xì)介紹。
淺克虏┩丁:
在淺克隆中绸贡,如果原型對(duì)象的成員變量是值類型,將復(fù)制一份給克隆對(duì)象毅哗;如果原型對(duì)象的成員變量是引用類型恃轩,則將引用對(duì)象的地址復(fù)制一份給克隆對(duì)象,也就是說原型對(duì)象和克隆對(duì)象的成員變量指向相同的內(nèi)存地址黎做。
簡單來說叉跛,在淺克隆中,當(dāng)對(duì)象被復(fù)制時(shí)只復(fù)制它本身和其中包含的值類型的成員變量蒸殿,而引用類型的成員對(duì)象并沒有復(fù)制筷厘。
在Java語言中,通過覆蓋Object類的clone()方法可以實(shí)現(xiàn)淺克隆宏所。
深克滤盅蕖:
在深克隆中,無論原型對(duì)象的成員變量是值類型還是引用類型爬骤,都將復(fù)制一份給克隆對(duì)象充石,深克隆將原型對(duì)象的所有引用對(duì)象也復(fù)制一份給克隆對(duì)象。
簡單來說霞玄,在深克隆中骤铃,除了對(duì)象本身被復(fù)制外,對(duì)象所包含的所有成員變量也將復(fù)制坷剧。
在Java語言中惰爬,如果需要實(shí)現(xiàn)深克隆,可以通過序列化(Serialization)等方式來實(shí)現(xiàn)惫企。需要注意的是能夠?qū)崿F(xiàn)序列化的對(duì)象其類必須實(shí)現(xiàn)Serializable接口撕瞧,否則無法實(shí)現(xiàn)序列化操作。
實(shí)現(xiàn)深克隆
方式一,手動(dòng)對(duì)引用對(duì)象進(jìn)行克麓园妗:
@Override
protected Object clone() throws CloneNotSupportedException {
Pig pig = (Pig)super.clone();
//深克隆
pig.birthday = (Date) pig.birthday.clone();
return pig;
}
方式二巩掺,通過序列化的方式:
public class Pig implements Serializable {
private String name;
private Date birthday;
// ...省略 getter, setter等
protected Object deepClone() throws CloneNotSupportedException, IOException, ClassNotFoundException {
//將對(duì)象寫入流中
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bao);
oos.writeObject(this);
//將對(duì)象從流中取出
ByteArrayInputStream bis = new ByteArrayInputStream(bao.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (ois.readObject());
}
}
破壞單例模式
餓漢式單例模式如下:
public class HungrySingleton implements Serializable, Cloneable {
private final static HungrySingleton hungrySingleton;
static {
hungrySingleton = new HungrySingleton();
}
private HungrySingleton() {
if (hungrySingleton != null) {
throw new RuntimeException("單例構(gòu)造器禁止反射調(diào)用");
}
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
private Object readResolve() {
return hungrySingleton;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
使用反射獲取對(duì)象,測試如下
public class Test {
public static void main(String[] args) throws CloneNotSupportedException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
HungrySingleton hungrySingleton = HungrySingleton.getInstance();
Method method = hungrySingleton.getClass().getDeclaredMethod("clone");
method.setAccessible(true);
HungrySingleton cloneHungrySingleton = (HungrySingleton) method.invoke(hungrySingleton);
System.out.println(hungrySingleton);
System.out.println(cloneHungrySingleton);
}
}
輸出
com.designpattern.HungrySingleton@34c45dca
com.designpattern.HungrySingleton@52cc8049
可以看到页畦,通過原型模式胖替,我們把單例模式給破壞了,現(xiàn)在有兩個(gè)對(duì)象了
為了防止單例模式被破壞寇漫,我們可以:不實(shí)現(xiàn) Cloneable
接口;或者把 clone
方法改為如下
@Override
protected Object clone() throws CloneNotSupportedException {
return getInstance();
}
原型模式的典型應(yīng)用
-
Object
類中的clone
接口 -
Cloneable
接口的實(shí)現(xiàn)類殉摔,可以看到至少一千多個(gè)州胳,找?guī)讉€(gè)例子譬如:
ArrayList
對(duì) clone
的重寫如下:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
//...省略
}
調(diào)用 super.clone();
之后把 elementData
數(shù)據(jù) copy 了一份
同理,我們看看 HashMap
對(duì) clone
方法的重寫:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
@Override
public Object clone() {
HashMap<K,V> result;
try {
result = (HashMap<K,V>)super.clone();
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
result.reinitialize();
result.putMapEntries(this, false);
return result;
}
// ...省略...
}
mybatis 中的 org.apache.ibatis.cache.CacheKey
對(duì) clone
方法的重寫:
public class CacheKey implements Cloneable, Serializable {
private List<Object> updateList;
public CacheKey clone() throws CloneNotSupportedException {
CacheKey clonedCacheKey = (CacheKey)super.clone();
clonedCacheKey.updateList = new ArrayList(this.updateList);
return clonedCacheKey;
}
// ... 省略...
}
這里又要注意逸月,updateList
是 List<Object>
類型栓撞,所以可能是值類型的List,也可能是引用類型的List碗硬,克隆的結(jié)果需要注意是否為深克隆或者淺克隆
使用原始模式的時(shí)候一定要注意為深克隆還是淺克隆瓤湘。
原型模式總結(jié)
原型模式的主要優(yōu)點(diǎn)如下:
- 當(dāng)創(chuàng)建新的對(duì)象實(shí)例較為復(fù)雜時(shí),使用原型模式可以簡化對(duì)象的創(chuàng)建過程恩尾,通過復(fù)制一個(gè)已有實(shí)例可以提高新實(shí)例的創(chuàng)建效率弛说。
- 擴(kuò)展性較好,由于在原型模式中提供了抽象原型類翰意,在客戶端可以針對(duì)抽象原型類進(jìn)行編程木人,而將具體原型類寫在配置文件中,增加或減少產(chǎn)品類對(duì)原有系統(tǒng)都沒有任何影響冀偶。
- 原型模式提供了簡化的創(chuàng)建結(jié)構(gòu)醒第,工廠方法模式常常需要有一個(gè)與產(chǎn)品類等級(jí)結(jié)構(gòu)相同的工廠等級(jí)結(jié)構(gòu),而原型模式就不需要這樣进鸠,原型模式中產(chǎn)品的復(fù)制是通過封裝在原型類中的克隆方法實(shí)現(xiàn)的稠曼,無須專門的工廠類來創(chuàng)建產(chǎn)品。
- 可以使用深克隆的方式保存對(duì)象的狀態(tài)客年,使用原型模式將對(duì)象復(fù)制一份并將其狀態(tài)保存起來霞幅,以便在需要的時(shí)候使用(如恢復(fù)到某一歷史狀態(tài)),可輔助實(shí)現(xiàn)撤銷操作量瓜。
原型模式的主要缺點(diǎn)如下:
- 需要為每一個(gè)類配備一個(gè)克隆方法蝗岖,而且該克隆方法位于一個(gè)類的內(nèi)部,當(dāng)對(duì)已有的類進(jìn)行改造時(shí)榔至,需要修改源代碼抵赢,違背了“開閉原則”。
- 在實(shí)現(xiàn)深克隆時(shí)需要編寫較為復(fù)雜的代碼,而且當(dāng)對(duì)象之間存在多重的嵌套引用時(shí)铅鲤,為了實(shí)現(xiàn)深克隆划提,每一層對(duì)象對(duì)應(yīng)的類都必須支持深克隆,實(shí)現(xiàn)起來可能會(huì)比較麻煩邢享。
適用場景:
- 創(chuàng)建新對(duì)象成本較大(如初始化需要占用較長的時(shí)間鹏往,占用太多的CPU資源或網(wǎng)絡(luò)資源),新的對(duì)象可以通過原型模式對(duì)已有對(duì)象進(jìn)行復(fù)制來獲得骇塘,如果是相似對(duì)象伊履,則可以對(duì)其成員變量稍作修改。
- 如果系統(tǒng)要保存對(duì)象的狀態(tài)款违,而對(duì)象的狀態(tài)變化很小唐瀑,或者對(duì)象本身占用內(nèi)存較少時(shí),可以使用原型模式配合備忘錄模式來實(shí)現(xiàn)插爹。
- 需要避免使用分層次的工廠類來創(chuàng)建分層次的對(duì)象哄辣,并且類的實(shí)例對(duì)象只有一個(gè)或很少的幾個(gè)組合狀態(tài),通過復(fù)制原型對(duì)象得到新實(shí)例可能比使用構(gòu)造函數(shù)創(chuàng)建一個(gè)新實(shí)例更加方便赠尾。
參考:
劉偉:設(shè)計(jì)模式Java版
慕課網(wǎng)java設(shè)計(jì)模式精講 Debug 方式+內(nèi)存分析
后記
歡迎評(píng)論力穗、轉(zhuǎn)發(fā)、分享气嫁,您的支持是我最大的動(dòng)力
更多內(nèi)容可訪問我的個(gè)人博客:http://laijianfeng.org
關(guān)注【小旋鋒】微信公眾號(hào)当窗,及時(shí)接收博文推送