定義
原型模式屬于對象的創建模式。通過給出一個原型對象來指明所有創建的對象的類型,然后用復制這個原型對象的辦法創建出更多同類型的對象。這就是原型模式的用意
原型模式的結構
原型模式要求對象實現同一個可以“克隆”自身的接口,遮掩個就可以通過賦值一個實例對象本身來創建一個新的實例。
這樣一來,通過原型實例創建新的對象,就不再需要關心這個實例本身的類型,只要實現了克隆自身的方法,就可以通過這個方法獲取新的對象,而無需再去通過new
來創建。
原型對象有兩種表現形式:
- 簡單形式
- 登記形式
這兩種形式僅僅是原型模式的不同實現。
簡單形式的原型模式
原型模式涉及三個角色:
- 客戶(Client)角色:客戶類提出創建對象的請求。
- 抽象原型(Prototype)角色:這是一個抽象角色,通常由一個Java接口或者Java抽象類實現。此角色給出所有的具體原型類所需的接口。
- 具體原型(Concrete Prototype)角色:被復制的對象。此角色需要實現抽象原型角色要求的接口。
示例代碼
抽象原型角色
/**
* 抽象原型角色
*/
public abstract class Prototype {
private String id;
public Prototype(String id) {
this.id = id;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
/**
* 克隆自身的方法
* @return 一個從自身克隆出來的對象。
*/
public abstract Prototype clone();
}
具體原型角色
public class ConcreteProtype1 extends Prototype {
public ConcreteProtype1(String id) {
super(id);
}
public Prototype clone() {
Prototype prototype = new ConcreteProtype1(this.getId());
return prototype;
}
}
public class ConcreteProtype2 extends Prototype {
public ConcreteProtype2(String id) {
super(id);
}
public Prototype clone() {
Prototype prototype = new ConcreteProtype2(this.getId());
return prototype;
}
}
客戶端
public class Client {
public static void main(String[] args) {
ConcreteProtype1 protype1 = new ConcreteProtype1("Protype1");
ConcreteProtype1 protypeCopy1 = (ConcreteProtype1)protype1.clone();
System.out.println(protypeCopy1.getId());
System.err.println(protype1.toString());
System.err.println(protypeCopy1.toString());
ConcreteProtype2 protype2 = new ConcreteProtype2("Protype2");
ConcreteProtype2 protypeCopy2 = (ConcreteProtype2)protype2.clone();
System.out.println(protypeCopy2.getId());
System.err.println(protype2.toString());
System.err.println(protypeCopy2.toString());
}
}
輸出結果:
Protype1
com.sschen.prototype.ConcreteProtype1@2a139a55
com.sschen.prototype.ConcreteProtype1@15db9742
Protype2
com.sschen.prototype.ConcreteProtype2@6d06d69c
com.sschen.prototype.ConcreteProtype2@7852e922
還有另外一種對象的復制方式,如下:
ConcreteProtype1 protype3 = new ConcreteProtype1("Protype3");
ConcreteProtype1 protypeCopy3 = protype3;
System.out.println(protypeCopy3.getId());
System.err.println(protype3.toString());
System.err.println(protypeCopy3.toString());
這種方式的輸出結果為:
Protype3
com.sschen.prototype.ConcreteProtype1@2a139a55
com.sschen.prototype.ConcreteProtype1@2a139a55
這種方式同上面的原型模式的克隆模式比較,可以看見的是:原型模式生成的兩個對象,內存地址是不一樣的。
之前在java面試 內存中堆和棧的區別文章中說明過:對象的值存放在堆中,在棧中存儲指向堆中內存位置的引用。
第一種方式中,我們是重新創建了新的對象,對象的值和引用都是新的,對于克隆對象的任何變更,都不會影響到原對象的值。如下圖:
另一種方式,我們只是在棧中新創建了一個引用,指向的還是被復制對象對應的堆地址,對于復制對象的變更,都會同時改變原對象的值。如下圖:
登記形式的原型模型
作為原型模式的第二種形式,它多了一個原型管理器(PrototypeManager)角色。該角色的作用是:創建具體有原型類的對象,并記錄每一個被創建的對象。
示例代碼
抽象原型角色
public interface Prototype {
public Prototype clone();
public String getName();
public void setName(String name);
}
具體原型角色
public class ConcretePrototype1 implements Prototype {
private String name;
@Override
public Prototype clone() {
ConcretePrototype1 prototype1 = new ConcretePrototype1();
prototype1.setName(this.name);
return prototype1;
}
@Override
public String getName() {
return this.name;
}
@Override
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "ConcretePrototype1 [name=" + name + "]";
}
}
public class ConcretePrototype2 implements Prototype {
private String name;
@Override
public Prototype clone() {
ConcretePrototype2 prototype2 = new ConcretePrototype2();
prototype2.setName(this.name);
return prototype2;
}
@Override
public String getName() {
return this.name;
}
@Override
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "ConcretePrototype2 [name=" + name + "]";
}
}
原型管理器角色保持一個聚集,作為對所有原型對象的登記,這個角色提供必要的方法,供外界增加新的原型對象和取得已經登記過的原型對象。
public class PrototypeManager {
/**
* 用來記錄原型的編號同原型實例的對象關系
*/
private static Map<String, Prototype> map = new HashMap<String, Prototype>();
/**
* 私有化構造方法,避免從外部創建實例
*/
private PrototypeManager() {}
/**
* 向原型管理器里面添加或者修改原型實例
* @param prototypeId 原型編號
* @param prototype 原型實例
*/
public synchronized static void setPrototype(String prototypeId, Prototype prototype) {
map.put(prototypeId, prototype);
}
/**
* 根據原型編號從原型管理器里面移除原型實例
* @param prototypeId 原型編號
*/
public synchronized static void removePrototype(String prototypeId) {
map.remove(prototypeId);
}
/**
* 根據原型編號獲取原型實例
* @param prototypeId 原型編號
* @return 原型實例對象
* @throws Exception 如果根據原型編號無法獲取對應實例,則提示異常“您希望獲取的原型還沒有注冊或已被銷毀”
*/
public synchronized static Prototype getPrototype(String prototypeId) throws Exception {
Prototype prototype = map.get(prototypeId);
if (prototype == null) {
throw new Exception("您希望獲取的原型還沒有注冊或已被銷毀");
}
return prototype;
}
}
客戶端角色
public class Client {
public static void main(String[] args) {
try {
Prototype p1 = new ConcretePrototype1();
PrototypeManager.setPrototype("p1", p1);
//獲取原型來創建對象
Prototype p3 = PrototypeManager.getPrototype("p1").clone();
p3.setName("張三");
System.out.println("第一個實例:" + p3);
//有人動態的切換了實現
Prototype p2 = new ConcretePrototype2();
PrototypeManager.setPrototype("p1", p2);
//重新獲取原型來創建對象
Prototype p4 = PrototypeManager.getPrototype("p1").clone();
p4.setName("李四");
System.out.println("第二個實例:" + p4);
//有人注銷了這個原型
PrototypeManager.removePrototype("p1");
//再次獲取原型來創建對象
Prototype p5 = PrototypeManager.getPrototype("p1").clone();
p5.setName("王五");
System.out.println("第三個實例:" + p5);
} catch (Exception e) {
e.printStackTrace();
}
}
}
輸出結果為:
第一個實例:ConcretePrototype1 [name=張三]
第二個實例:ConcretePrototype2 [name=李四]
java.lang.Exception: 您希望獲取的原型還沒有注冊或已被銷毀
at com.sschen.prototyperegist.PrototypeManager.getPrototype(PrototypeManager.java:44)
at com.sschen.prototyperegist.Client.main(Client.java:26)
兩種形式的比較
簡單形式和登記形式的原型模式各有其長處和短處。
- 如果需要創建的原型對象數據較少而且比較固定的話,可以采用第一種形式。在這種情況下,原型對象的引用可以由客戶端自己保存。
- 如果要創建的原型對象數據不固定的話,可以采用第二種形式。在這種情況下,客戶端不保存對原型對象的引用,這個任務被交給原型管理器角色。在克隆一個對象之前,客戶端可以查看管理員對象是否已經有一個滿足要求的原型對象。如果有,可以從原型管理器角色中取得這個對象引用;如果沒有,客戶端就需要自行復制此原型對象。
Java中的克隆方法
Java中的所有類都是從java.lang.Object
類繼承而來的,而Object
類提供protected Object clone()
方法對對象進行克隆復制,子類當然也可以把這個方法置換掉,提供滿足自己需要的復制方法。對象的復制有一個基本問題,就是對象通常都有對其他對象的引用。當使用Object
類的clone()
方法來復制一個對象時,此對象對其他對象的引用也同時會被復制一份。
java語言提供的Cloneable
接口只起一個作用,就是在運行時期通知Java虛擬機可以安全的在這個類上使用clone()
方法。通過調用這個clone()
方法可以得到一個對象的復制。由于Object
類本身不實現Cloneable
接口,因此如果所考慮的類沒有實現Cloneable
接口時,調用clone()
方法會拋出CloneNotSupportedException
異常。
克隆滿足的條件
clone()
方法將對象復制了一份并返還給了調用者。所謂“復制”的含義于clone()
方法是怎么實現的含義是一樣的。一般而言,clone()
方法滿足以下的描述:
- 對任何的對象
x
,都有x.clone() != x
。換而言之,克隆對象和原對象不是同一個對象。 - 對任何的對象
x
,都有x.clone().getClass() == x.getClass()
。換而言之,克隆對象同原對象的類型一致。 - 如果對象
x
的equals()
方法定義其恰當的話,那么x.clone().equals(x)
應當是成立的。
在Java語言的API中,凡是提供了clone()
方法的類,都滿足上面的這些條件。Java語言的設計師再設計自己的clone()
方法時,也應當遵守這三個條件。一般來說,上面的三個條件中的前兩個是必需的,而第三個是可選的。
淺克隆和深克隆
無論你是自己實現克隆方法,還是采用Java提供的克隆方法,都存在一個淺度克隆和深度克隆的問題。
-
淺度克隆:只負責克隆按值傳遞的數據(比如基本數據類型,
String
類型),而不是復制它所引用的對象。換而言之,所有對其他對象的引用都仍然指向原來的對象。 -
深度克隆:除了淺度克隆需要克隆的值外,還負責克隆引用類型的數據。那些引用其他對象的變量將指向被復制過的新對象,而不再是原有的那些被引用的對象。換而言之,深度克隆要把復制的對象所引用的對象都復制一遍,而這種對被引用到的對象的復制叫做簡間接復制。
深度克隆要深入到多少層,是一個不易確定的問題。在決定以深度克隆的方式復制一個對象的時候,必須決定對間接復制的對象是采取淺度克隆還是繼續采用深度克隆。因此,在采用深度克隆時,需要決定多深才算深。
此外,在深度克隆的過程中,很可能會出現循環引用的問題,必須小心處理。
使用序列化實現深度克隆
把對象寫到流里的過程是序列化Serialization
的過程;而把對象從流中讀出來的過程叫反序列化Deserialization
過程。應當指出的是,寫到流里的是對象的一個拷貝,原對象仍然存在于JVM中。
在Java語言里深度克隆一個對象,常常可以先使對象實現Serializable
接口,然后把對象(實際上對象的拷貝)寫到一個流里(序列化過程),再從流里讀出來(反序列化過程),便可以重建對象。
public Object deepClone() throws IOException, ClassNotFoundException{
//將對象寫到流里
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
//從流里讀回來
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject();
}
這樣做的前提就是對象及對象內部所有引用到的對象都是可序列化的,否則,就需要仔細考察那些不可序列化的對象是否可以設置成transient
,從而將至排除在復制過程之外。
淺度克隆顯然比深度克隆更容易實現,因為Java語言的所有類都會繼承一個clone()
方法,而這個clone()
方法所做的正是淺度克隆。
有一些對象,比如線程Thread
對象或者Socket
對象,是不能簡單復制或者共享的。不管是使用淺度克隆還是使用深度克隆,只要涉及這樣的間接對象,就必須把簡介對象射程transient
而不予復制;或者由程序自行創建出相當的同等對象,權且當做復制件使用。
孫大圣的身外身法術
孫大圣的身外身本領如果在Java語言里使用原型模式來實現的話,會怎么樣呢?
首先,齊天大圣The Greatest Sage
,即TheGreatestSage類扮演客戶角色。齊天大圣持有一個猢猻Monkey
的實例,而猢猻就是大圣本尊。Monkey
類具有繼承自java.lang.Object
的clone()
方法,因此,可以通過調用這個克隆方法來復制一個Monkey
實例。
示例代碼
大圣持有金箍棒的實例,因此這里定義了一個金箍棒的類GoldRingedStaff
/**
* 金箍棒對象
*/
public class GoldRingedStaff {
/**
* 高度
*/
private float height = 100.00f;
/**
* 半徑
*/
private float radius = 10.00f;
/**
* 金箍棒變大方法
*/
public void grow() {
this.radius *= 2;
this.height *= 2;
}
/**
* 金箍棒縮小方法
*/
public void shrink() {
this.radius /= 2;
this.height /= 2;
}
}
大圣本尊使用Monkey
類來表示,這個類來扮演具體的原型角色:
/**
* 獼猴類,大圣本尊由獼猴類來表示
*/
public class Monkey implements Cloneable {
/**
* 身高
*/
private int height;
/**
* 體重
*/
private int weight;
/**
* 出生日期
*/
private Date birthDay;
/**
* 金箍棒
*/
private GoldRingedStaff staff;
/**
* 構造函數,指定創建事件和給定金箍棒
*/
public Monkey() {
this.birthDay = new Date();
this.staff = new GoldRingedStaff();
}
/**
* 克隆方法,直接調用接口的克隆方法
*/
public Object clone() {
Monkey temp = null;
try {
temp = (Monkey)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
} finally {
return temp;
}
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
public Date getBirthDay() {
return birthDay;
}
public void setBirthDay(Date birthDay) {
this.birthDay = birthDay;
}
public GoldRingedStaff getStaff() {
return staff;
}
public void setStaff(GoldRingedStaff staff) {
this.staff = staff;
}
}
孫大圣本尊則使用類TheGreatestSage
來表示:
public class TheGreatestSage {
private Monkey monkey = new Monkey();
public void change() {
Monkey copyMonkey = (Monkey) monkey.clone();
System.out.println("大圣本尊的生日是:" + monkey.getBirthDay());
System.out.println("克隆大圣的生日是:" + copyMonkey.getBirthDay());
System.out.println("大圣本尊同克隆大圣是否為同一個對象:" + (monkey == copyMonkey));
System.out.println("大圣本尊持有的金箍棒 同 克隆大圣持有的金箍棒是否為同一個對象:" + (monkey.getStaff() == copyMonkey.getStaff()));
}
public static void main(String[] args) {
TheGreatestSage sage = new TheGreatestSage();
sage.change();
}
}
當運行TheGreatestSage
類時,首先創建大圣本尊對象,然后淺度克隆大圣本尊對象。程序在運行時輸出的信息如下:
大圣本尊的生日是:Wed Jun 28 10:19:53 CST 2017
克隆大圣的生日是:Wed Jun 28 10:19:53 CST 2017
大圣本尊同克隆大圣是否為同一個對象:false
大圣本尊持有的金箍棒 同 克隆大圣持有的金箍棒是否為同一個對象:true
可以看出,首先,復制的大圣本尊具有和原始的大圣本尊一樣的birthDay
,而本尊對象不相等,這表明他們二者是克隆關系;其次,復制的大圣本尊持有的金箍棒和原始大圣持有的金箍棒為同一個對象,這表明二者所持有的金箍棒為同一根,而非兩根。
正如前面所述,繼承自java.lang.Object
類的clone()
方法是淺度克隆。換而言之,齊天大圣所有化身所持有的金箍棒引用全都是指向一個對象的,這與《西游記》中描寫并不一致。要想要糾正這一點,就需要考慮使用深度克隆
想要做到深度克隆,就要求所有需要復制的對象都實現java.io.Serializable
接口。
實例代碼,修改為深度克隆后
孫大圣的源代碼
public class TheGreatestSage {
private Monkey monkey = new Monkey();
public void change() {
Monkey copyMonkey = null;
try {
copyMonkey = (Monkey) monkey.deepClone();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("大圣本尊的生日是:" + monkey.getBirthDay());
System.out.println("克隆大圣的生日是:" + copyMonkey.getBirthDay());
System.out.println("大圣本尊同克隆大圣是否為同一個對象:" + (monkey == copyMonkey));
System.out.println("大圣本尊持有的金箍棒 同 克隆大圣持有的金箍棒是否為同一個對象:" + (monkey.getStaff() == copyMonkey.getStaff()));
}
public static void main(String[] args) {
TheGreatestSage sage = new TheGreatestSage();
sage.change();
}
}
在大圣本尊Monkey
類中,原有一個克隆方法clone()
,為淺度克隆,因此新增一個方法deepClone()
,為深度克隆。在deepClone()
方法中,大圣本尊對象(一個拷貝)被序列化,然后又被反序列化。反序列化后的對象也就成了一個深度克隆后的對象。deepClone()
方法如下:
/**
* 獼猴類,大圣本尊由獼猴類來表示
*/
public class Monkey implements Cloneable,Serializable {
//………………
/**
* 克隆方法,直接調用接口的克隆方法
*/
public Object clone() {
Monkey temp = null;
try {
temp = (Monkey)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
} finally {
return temp;
}
}
public Object deepClone() throws IOException, ClassNotFoundException {
//將對象寫入流中
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(this);
//將對象從流中讀取回來
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
return objectInputStream.readObject();
}
//……………………
}
對于金箍棒GoldRingedStaff
類,也讓其實現java.io.Serializable
接口:
public class GoldRingedStaff implements Serializable {
//………………
}
修改后的代碼運行結果為:
大圣本尊的生日是:Wed Jun 28 11:31:01 CST 2017
克隆大圣的生日是:Wed Jun 28 11:31:01 CST 2017
大圣本尊同克隆大圣是否為同一個對象:false
大圣本尊持有的金箍棒 同 克隆大圣持有的金箍棒是否為同一個對象:false
從運行結果可以看出,大圣的金箍棒同克隆大圣的金箍棒是不同的對象。這是因為使用了深度克隆,從而將大圣本尊所引用的對象也都復制了一遍,其中也包括金箍棒。
原型模式優缺點總結
原型模式的優點
原型模式允許在運行時動態的改變具體的實現類型。原型模式可以在運行期間,有客戶來注冊符合原型接口的實現類型,也可以動態的改變具體的實現類型,看起來接口沒有任何變化,但是其實運行的已經是另外一個類實體了。因為克隆一個原型對象就類似于實例化一個類。
原型模式的缺點
原型模式最主要的缺點是每一個類都必須要配備一個克隆方法。配備克隆方法需要對類的功能進行通盤考慮,這對于全新的類來說并不是很難,但是對于已有的類來說并不容易,可別是當一個類引用不支持序列化的間接對象,或者引用含有循環結構的時候。