## 引入
在閻宏博士的《JAVA與模式》一書中開頭是這樣描述原型(Prototype)模式的:
> 原型模式屬于對象的創建模式。通過給出一個原型對象來指明所有創建的對象的類型,然后用復制這個原型對象的辦法創建出更多同類型的對象。這就是選型模式的用意。
克隆我們都清楚,就是用一個物體復制若干個一模一樣物體。同樣,在面向對象系統中,我們同樣可以利用克隆技術來克隆出若干個一模一樣的對象。在應用程序中,有些對象比較復雜,其創建過程過于復雜,而且我們又需要頻繁的利用該對象,如果這個時候我們按照常規思維new該對象,那么務必會帶來非常多的麻煩,這個時候我們就希望可以利用一個已有的對象來不斷對他進行復制就好了,這就是編程中的“克隆”。這里原型模式就可以滿足我們的“克隆”,在原型模式中我們可以利用過一個原型對象來指明我們所要創建對象的類型,然后通過復制這個對象的方法來獲得與該對象一模一樣的對象實例。這就是原型模式的設計目的。
## 模式定義
所謂原型模式就是用原型實例指定創建對象的種類,并且通過復制這些原型創建新的對象。(主要用于對象的復制)
## 結構
原型模式要求對象實現一個可以“克隆”自身的接口,這樣就可以通過復制一個實例對象本身來創建一個新的實例。這樣一來,通過原型實例創建新的對象,就不再需要關心這個實例本身的類型,只要實現了克隆自身的方法,就可以通過這個方法來獲取新的對象,而無須再去通過new來創建。
原型模式有兩種表現形式:(1)簡單形式、(2)登記形式,這兩種表現形式僅僅是原型模式的不同實現。
**簡單形式的原型模式**

這種形式涉及到三個角色:
(1)客戶(Client)角色:客戶類提出創建對象的請求。
(2)抽象原型(Prototype)角色:這是一個抽象角色,通常由一個Java接口或Java抽象類實現。此角色給出所有的具體原型類所需的接口。
(3)具體原型(Concrete Prototype)角色:被復制的對象。此角色需要實現抽象的原型角色所要求的接口。
**登記形式的原型模式**

作為原型模式的第二種形式,它多了一個原型管理器(PrototypeManager)角色,該角色的作用是:創建具體原型類的對象,并記錄每一個被創建的對象。
**兩種形式的比較**
簡單形式和登記形式的原型模式各有其長處和短處。
如果需要創建的原型對象數目較少而且比較固定的話,可以采取第一種形式。在這種情況下,原型對象的引用可以由客戶端自己保存。
如果要創建的原型對象數目不固定的話,可以采取第二種形式。在這種情況下,客戶端不保存對原型對象的引用,這個任務被交給管理員對象。在復制一個原型對象之前,客戶端可以查看管理員對象是否已經有一個滿足要求的原型對象。如果有,可以直接從管理員類取得這個對象引用;如果沒有,客戶端就需要自行復制此原型對象。
## 代碼實現
利用原型模式來模擬復印簡歷。
~~~
public class Resume implements Cloneable {
private String name;
private String birthday;
private String sex;
private String school;
private String timeArea;
private String company;
/**
* 構造函數:初始化簡歷賦值姓名
*/
public Resume(String name){
this.name = name;
}
/**
* @desc 設定個人基本信息
* @param birthday 生日
* @param sex 性別
* @param school 畢業學校
* @return void
*/
public void setPersonInfo(String birthday,String sex,String school){
this.birthday = birthday;
this.sex = sex;
this.school = school;
}
/**
* @desc 設定工作經歷
* @param timeArea 工作年限
* @param company 所在公司
* @return void
*/
public void setWorkExperience(String timeArea,String company){
this.timeArea = timeArea;
this.company = company;
}
/**
* 克隆該實例
*/
public Object clone(){
Resume resume = null;
try {
resume = (Resume) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return resume;
}
public void display(){
System.out.println("姓名:" + name);
System.out.println("生日:" + birthday + ",性別:" + sex + ",畢業學校:" + school);
System.out.println("工作年限:" + timeArea + ",公司:" + company);
}
}
~~~
客戶端:Client.java
~~~
public class Client {
public static void main(String[] args) {
//原型A對象
Resume a = new Resume("小李子");
a.setPersonInfo("2.16", "男", "XX大學");
a.setWorkExperience("2012.09.05", "XX科技有限公司");
//克隆B對象
Resume b = (Resume) a.clone();
//輸出A和B對象
System.out.println("----------------A--------------");
a.display();
System.out.println("----------------B--------------");
b.display();
/*
* 測試A==B?
* 對任何的對象x,都有x.clone() !=x,即克隆對象與原對象不是同一個對象
*/
System.out.print("A==B?");
System.out.println( a == b);
/*
* 對任何的對象x,都有x.clone().getClass()==x.getClass(),即克隆對象與原對象的類型一樣。
*/
System.out.print("A.getClass()==B.getClass()?");
System.out.println(a.getClass() == b.getClass());
}
}
~~~
### 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而不予復制;或者由程序自行創建出相當的同種對象,權且當做復制件使用。
## 注意事項
* 使用原型模式復制對象不會調用類的構造方法。因為對象的復制是通過調用Object類的clone方法來完成的,它直接在內存中復制數據,因此不會調用到類的構造方法。不但構造方法中的代碼不會執行,甚至連訪問權限都對原型模式無效。單例模式中,只要將構造方法的訪問權限設置為private型,就可以實現單例。但是clone方法直接無視構造方法的權限,所以,單例模式與原型模式是沖突的,在使用時要特別注意。
* 深拷貝與淺拷貝。Object類的clone方法只會拷貝對象中的基本的數據類型(8種基本數據類型byte,char,short,int,long,float,double,boolean),對于數組、容器對象、引用對象等都不會拷貝,這就是淺拷貝。如果要實現深拷貝,必須將原型模式中的數組、容器對象、引用對象等另行拷貝。例如:
* 由于ArrayList不是基本類型,所以成員變量list,不會被拷貝,需要我們自己實現深拷貝,幸運的是java提供的大部分的容器類都實現了Cloneable接口。
## 優點
* 1、如果創建新的對象比較復雜時,可以利用原型模式簡化對象的創建過程,同時也能夠提高效率。使用原型模式創建對象比直接new一個對象在性能上要好的多.這是因為Object類的clone方法時一個本地方法.他直接操作內存中的二進制流.這點在復制復雜或者耗時的實例時,性能差別非常明顯.
* 2、可以使用深克隆保持對象的狀態。
* 3、原型模式提供了簡化的創建結構。
## 缺點
* 1、在實現深克隆的時候可能需要比較復雜的代碼。
* 2、需要為每一個類配備一個克隆方法,而且這個克隆方法需要對類的功能進行通盤考慮,這對全新的類來說不是很難,但對已有的類進行改造時,不一定是件容易的事,必須修改其源代碼,違背了“開閉原則”。
## 使用場景
* 1、如果類初始化需要消耗相當多的資源,包括數據,硬件資等,即創建新對象成本較大,我們可以利用已有的對象進行復制來獲得。
* 2、有性能和安全要求的場景。如果系統要保存對象的狀態,而對象的狀態變化很小,或者對象本身占內存不大的時候,也可以使用原型模式配合備忘錄模式來應用。相反,如果對象的狀態變化很大,或者對象占用的內存很大,那么采用狀態模式會比原型模式更好。
* 3、通過new產生一個對象需要非常繁瑣的數據準備和訪問權限時。
* 4、需要避免使用分層次的工廠類來創建分層次的對象,并且類的實例對象只有一個或很少的幾個組合狀態,通過復制原型對象得到新實例可能比使用構造函數創建一個新實例更加方便。
## 總結
* 1、原型模式向客戶隱藏了創建對象的復雜性。客戶只需要知道要創建對象的類型,然后通過請求就可以獲得和該對象一模一樣的新對象,無須知道具體的創建過程。
* 2、克隆分為淺克隆和深克隆兩種。
* 3、我們雖然可以利用原型模式來獲得一個新對象,但有時對象的復制可能會相當的復雜,比如深克隆。
- java
- 設計模式
- 設計模式總覽
- 設計原則
- 工廠方法模式
- 抽象工廠模式
- 單例模式
- 建造者模式
- 原型模式
- 適配器模式
- 裝飾者模式
- 代理模式
- 外觀模式
- 橋接模式
- 組合模式
- 享元模式
- 策略模式
- 模板方法模式
- 觀察者模式
- 迭代子模式
- 責任鏈模式
- 命令模式
- 備忘錄模式
- 狀態模式
- 訪問者模式
- 中介者模式
- 解釋器模式
- 附錄
- JVM相關
- JVM內存結構
- Java虛擬機的內存組成以及堆內存介紹
- Java堆和棧
- 附錄-數據結構的堆棧和內存分配的堆區棧區的區別
- Java內存之Java 堆
- Java內存之虛擬機和內存區域概述
- Java 內存之方法區和運行時常量池
- Java 內存之直接內存(堆外內存)
- JAVA內存模型
- Java內存模型介紹
- 內存模型如何解決緩存一致性問題
- 深入理解Java內存模型——基礎
- 深入理解Java內存模型——重排序
- 深入理解Java內存模型——順序一致性
- 深入理解Java內存模型——volatile
- 深入理解Java內存模型——鎖
- 深入理解Java內存模型——final
- 深入理解Java內存模型——總結
- 內存可見性
- JAVA對象模型
- JVM內存結構 VS Java內存模型 VS Java對象模型
- Java的對象模型
- Java的對象頭
- HotSpot虛擬機
- HotSpot虛擬機對象探秘
- 深入分析Java的編譯原理
- Java虛擬機的鎖優化技術
- 對象和數組并不是都在堆上分配內存的
- 垃圾回收
- JVM內存管理及垃圾回收
- JVM 垃圾回收器工作原理及使用實例介紹
- JVM內存回收理論與實現(對象存活的判定)
- JVM參數及調優
- CMS GC日志分析
- JVM實用參數(一)JVM類型以及編譯器模式
- JVM實用參數(二)參數分類和即時(JIT)編譯器診斷
- JVM實用參數(三)打印所有XX參數及值
- JVM實用參數(四)內存調優
- JVM實用參數(五)新生代垃圾回收
- JVM實用參數(六) 吞吐量收集器
- JVM實用參數(七)CMS收集器
- JVM實用參數(八)GC日志
- Java性能調優原則
- JVM 優化經驗總結
- 面試題整理
- 面試題1
- java日志規約
- Spring安全
- OAtuth2.0簡介
- Spring Session 簡介(一)
- Spring Session 簡介(二)
- Spring Session 簡介(三)
- Spring Security 簡介(一)
- Spring Security 簡介(二)
- Spring Security 簡介(三)
- Spring Security 簡介(四)
- Spring Security 簡介(五)
- Spring Security Oauth2 (一)
- Spring Security Oauth2 (二)
- Spring Security Oauth2 (三)
- SpringBoot
- Shiro
- Shiro和Spring Security對比
- Shiro簡介
- Session、Cookie和Cache
- Web Socket
- Spring WebFlux