在閻宏博士的《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與模式》之原型模式