在閻宏博士的《JAVA與模式》一書中開頭是這樣描述原型(Prototype)模式的:
原型模式屬于對象的建立模式。通過給出一個原型對象來指明所有建立的對象的類型,然後用複制這個原型對象的辦法建立出更多同類型的對象。這就是選型模式的用意。
原型模式的結構
原型模式要求對象實作一個可以“克隆”自身的接口,這樣就可以通過複制一個執行個體對象本身來建立一個新的執行個體。這樣一來,通過原型執行個體建立新的對象,就不再需要關心這個執行個體本身的類型,隻要實作了克隆自身的方法,就可以通過這個方法來擷取新的對象,而無須再去通過new來建立。
原型模式有兩種表現形式:(1)簡單形式、(2)登記形式,這兩種表現形式僅僅是原型模式的不同實作。
簡單形式的原型模式
這種形式涉及到三個角色:
(1)客戶(Client)角色:客戶類提出建立對象的請求。
(2)抽象原型(Prototype)角色:這是一個抽象角色,通常由一個Java接口或Java抽象類實作。此角色給出所有的具體原型類所需的接口。
(3)具體原型(Concrete Prototype)角色:被複制的對象。此角色需要實作抽象的原型角色所要求的接口。
源代碼
抽象原型角色
public interface Prototype{
/**
* 克隆自身的方法
* @return 一個從自身克隆出來的對象
*/
public Object clone();
}
具體原型角色
public class ConcretePrototype1 implements Prototype {
public Prototype clone(){
//最簡單的克隆,建立一個自身對象,由于沒有屬性就不再複制值了
Prototype prototype = new ConcretePrototype1();
return prototype;
}
}
public class ConcretePrototype2 implements Prototype {
public Prototype clone(){
//最簡單的克隆,建立一個自身對象,由于沒有屬性就不再複制值了
Prototype prototype = new ConcretePrototype2();
return prototype;
}
}
用戶端角色
public class Client {
/**
* 持有需要使用的原型接口對象
*/
private Prototype prototype;
/**
* 構造方法,傳入需要使用的原型接口對象
*/
public Client(Prototype prototype){
this.prototype = prototype;
}
public void operation(Prototype example){
//需要建立原型接口的對象
Prototype copyPrototype = prototype.clone();
}
}
登記形式的原型模式
作為原型模式的第二種形式,它多了一個原型管理器(PrototypeManager)角色,該角色的作用是:建立具體原型類的對象,并記錄每一個被建立的對象。
public interface Prototype{
public Prototype clone();
public String getName();
public void setName(String name);
}
public class ConcretePrototype1 implements Prototype {
private String name;
public Prototype clone(){
ConcretePrototype1 prototype = new ConcretePrototype1();
prototype.setName(this.name);
return prototype;
}
public String toString(){
return "Now in Prototype1 , name = " + this.name;
}
@Override
public String getName() {
return name;
}
@Override
public void setName(String name) {
this.name = name;
}
}
public class ConcretePrototype2 implements Prototype {
private String name;
public Prototype clone(){
ConcretePrototype2 prototype = new ConcretePrototype2();
prototype.setName(this.name);
return prototype;
}
public String toString(){
return "Now in Prototype2 , name = " + this.name;
}
@Override
public String getName() {
return name;
}
@Override
public void setName(String name) {
this.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();
}
}
}
兩種形式的比較
簡單形式和登記形式的原型模式各有其長處和短處。
如果需要建立的原型對象數目較少而且比較固定的話,可以采取第一種形式。在這種情況下,原型對象的引用可以由用戶端自己儲存。
如果要建立的原型對象數目不固定的話,可以采取第二種形式。在這種情況下,用戶端不儲存對原型對象的引用,這個任務被交給管理者對象。在複制一個原型對象之前,用戶端可以檢視管理者對象是否已經有一個滿足要求的原型對象。如果有,可以直接從管理者類取得這個對象引用;如果沒有,用戶端就需要自行複制此原型對象。
Java中的克隆方法
Java的所有類都是從java.lang.Object類繼承而來的,而Object類提供protected Object clone()方法對對象進行複制,子類當然也可以把這個方法置換掉,提供滿足自己需要的複制方法。對象的複制有一個基本問題,就是對象通常都有對其他的對象的引用。當使用Object類的clone()方法來複制一個對象時,此對象對其他對象的引用也同時會被複制一份
Java語言提供的Cloneable接口隻起一個作用,就是在運作時期通知Java虛拟機可以安全地在這個類上使用clone()方法。通過調用這個clone()方法可以得到一個對象的複制。由于Object類本身并不實作Cloneable接口,是以如果所考慮的類沒有實作Cloneable接口時,調用clone()方法會抛出CloneNotSupportedException異常。
克隆滿足的條件
clone()方法将對象複制了一份并返還給調用者。所謂“複制”的含義與clone()方法是怎麼實作的。一般而言,clone()方法滿足以下的描述:
(1)對任何的對象x,都有:x.clone()!=x。換言之,克隆對象與原對象不是同一個對象。
(2)對任何的對象x,都有:x.clone().getClass() == x.getClass(),換言之,克隆對象與原對象的類型一樣。
(3)如果對象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執行個體。
孫大聖本人用TheGreatestSage類代表
public class TheGreatestSage {
private Monkey monkey = new Monkey();
public void change(){
//克隆大聖本尊
Monkey copyMonkey = (Monkey)monkey.clone();
System.out.println("大聖本尊的生日是:" + monkey.getBirthDate());
System.out.println("克隆的大聖的生日是:" + monkey.getBirthDate());
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類代表,這個類扮演具體原型角色:
public class Monkey implements Cloneable {
//身高
private int height;
//體重
private int weight;
//生日
private Date birthDate;
//金箍棒
private GoldRingedStaff staff;
/**
* 構造函數
*/
public Monkey(){
this.birthDate = new Date();
this.staff = new GoldRingedStaff();
}
/**
* 克隆方法
*/
public Object clone(){
Monkey temp = null;
try {
temp = (Monkey) super.clone();
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
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 getBirthDate() {
return birthDate;
}
public void setBirthDate(Date birthDate) {
this.birthDate = birthDate;
}
public GoldRingedStaff getStaff() {
return staff;
}
public void setStaff(GoldRingedStaff staff) {
this.staff = staff;
}
}
大聖還持有一個金箍棒的執行個體,金箍棒類GoldRingedStaff:
public class GoldRingedStaff {
private float height = 100.0f;
private float diameter = 10.0f;
/**
* 增長行為,每次調用長度和半徑增加一倍
*/
public void grow(){
this.diameter *= 2;
this.height *= 2;
}
/**
* 縮小行為,每次調用長度和半徑減少一半
*/
public void shrink(){
this.diameter /= 2;
this.height /= 2;
}
}
當運作TheGreatestSage類時,首先建立大聖本尊對象,而後淺度克隆大聖本尊對象。程式在運作時列印出的資訊如下:
可以看出,首先,複制的大聖本尊具有和原始的大聖本尊對象一樣的birthDate,而本尊對象不相等,這表明他們二者是克隆關系;其次,複制的大聖本尊所持有的金箍棒和原始的大聖本尊所持有的金箍棒為同一個對象。這表明二者所持有的金箍棒根本是一根,而不是兩根。
正如前面所述,繼承自java.lang.Object類的clone()方法是淺克隆。換言之,齊天大聖的所有化身所持有的金箍棒引用全都是指向一個對象的,這與《西遊記》中的描寫并不一緻。要糾正這一點,就需要考慮使用深克隆。
為做到深度克隆,所有需要複制的對象都需要實作java.io.Serializable接口。
孫大聖的源代碼:
public class TheGreatestSage {
private Monkey monkey = new Monkey();
public void change() throws IOException, ClassNotFoundException{
Monkey copyMonkey = (Monkey)monkey.deepClone();
System.out.println("大聖本尊的生日是:" + monkey.getBirthDate());
System.out.println("克隆的大聖的生日是:" + monkey.getBirthDate());
System.out.println("大聖本尊跟克隆的大聖是否為同一個對象 " + (monkey == copyMonkey));
System.out.println("大聖本尊持有的金箍棒 跟 克隆的大聖持有的金箍棒是否為同一個對象? " + (monkey.getStaff() == copyMonkey.getStaff()));
}
public static void main(String[]args) throws IOException, ClassNotFoundException{
TheGreatestSage sage = new TheGreatestSage();
sage.change();
}
}
在大聖本尊Monkey類裡面,有兩個克隆方法,一個是clone(),也即淺克隆;另一個是deepClone(),也即深克隆。在深克隆方法裡,大聖本尊對象(一個拷貝)被序列化,然後又被反序列化。反序列化的對象就成了一個深克隆的結果。
public class Monkey implements Cloneable,Serializable {
//身高
private int height;
//體重
private int weight;
//生日
private Date birthDate;
//金箍棒
private GoldRingedStaff staff;
/**
* 構造函數
*/
public Monkey(){
this.birthDate = new Date();
staff = new GoldRingedStaff();
}
/**
* 克隆方法
*/
public Object clone(){
Monkey temp = null;
try {
temp = (Monkey) super.clone();
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
return temp;
}
}
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();
}
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 getBirthDate() {
return birthDate;
}
public void setBirthDate(Date birthDate) {
this.birthDate = birthDate;
}
public GoldRingedStaff getStaff() {
return staff;
}
public void setStaff(GoldRingedStaff staff) {
this.staff = staff;
}
}
可以看到,大聖本尊持有一個金箍棒(GoldRingedStaff)的執行個體。在大聖複制件裡面,此金箍棒執行個體是原大聖本尊對象所持有的金箍棒對象的一個拷貝。在大聖本尊對象被序列化和反序列化時,它所持有的金箍棒對象也同時被序列化和反序列化,這使得複制的大聖的金箍棒和原大聖本尊對象所持有的金箍棒對象是兩個獨立的對象。
public class GoldRingedStaff implements Serializable{
private float height = 100.0f;
private float diameter = 10.0f;
/**
* 增長行為,每次調用長度和半徑增加一倍
*/
public void grow(){
this.diameter *= 2;
this.height *= 2;
}
/**
* 縮小行為,每次調用長度和半徑減少一半
*/
public void shrink(){
this.diameter /= 2;
this.height /= 2;
}
}
運作結果:
從運作的結果可以看出,大聖的金箍棒和他的身外之身的金箍棒是不同的對象。這是因為使用了深克隆,進而把大聖本尊所引用的對象也都複制了一遍,其中也包括金箍棒。
原型模式的優點
原型模式允許在運作時動态改變具體的實作類型。原型模式可以在運作期間,由客戶來注冊符合原型接口的實作類型,也可以動态地改變具體的實作類型,看起來接口沒有任何變化,但其實運作的已經是另外一個類執行個體了。因為克隆一個原型就類似于執行個體化一個類。
原型模式的缺點
原型模式最主要的缺點是每一個類都必須配備一個克隆方法。配備克隆方法需要對類的功能進行通盤考慮,這對于全新的類來說不是很難,而對于已經有的類不一定很容易,特别是當一個類引用不支援序列化的間接對象,或者引用含有循環結構的時候。
原文位址:《JAVA與模式》之原型模式