
1、簡介
定義:用原型實例指定創建對象的種類,並且通過拷貝這些原型來創建新的對象。
功能:①是通過克隆來創建新的對象實例;②是為克隆出來的新的對象實例復制原型實例屬性的值。
本質:通過克隆來創建新的對象實例。
英文:Prototype
類型:創建型
2、類圖及組成
(引)類圖:

組成:
Prototype:聲明一個克隆自身的接口,用來約束想要克隆自己的類,要求它們都要實現這里定義的克隆方法。
ConcretePrototype:實現Prototype接口的類,這些類真正實現了克隆自身的功能。
Client:使用原型的客戶端,首先要獲取到原型實例對象,然后通過原型實例克隆自身來創建新的對象實例。
標准代碼結構:
/** * 先來看看原型接口的定義 : 聲明一個克隆自身的接口 */ public interface Prototype { /** * 克隆自身的方法 * @return 一個從自身克隆出來的對象 */ public Prototype clone(); } /** * 具體的原型實現對象 : 克隆的具體實現對象 * 為了跟上面原型模式的結構示意圖保持一致,因此這兩個具體的原型實現對象,都沒有定義屬性。事實上,在實際使用原型模式的應用中,原型對象多是有屬性的,克隆原型的時候也是需要克隆原型對象的屬性的,特此說明一下。 */ public class ConcretePrototype implements Prototype { public Prototype clone() { //最簡單的克隆,新建一個自身對象,由於沒有屬性,就不去復制值了 Prototype prototype = new ConcretePrototype(); return prototype; } } /** * 使用原型的客戶端 */ public class Client { /** * 持有需要使用的原型接口對象 */ private Prototype prototype; /** * 構造方法,傳入需要使用的原型接口對象 * @param prototype 需要使用的原型接口對象 */ public Client(Prototype prototype){ this.prototype = prototype; } /** * 示意方法,執行某個功能操作 */ public void operation(){ //會需要創建原型接口的對象 Prototype newPrototype = prototype.clone(); } }
上面的代碼組成是為了讓大家清楚了解原型模式的結構,Java中已經提供Cloneable接口,所以在實現原型模式不必在實現Prototype原型接口,直接實現Cloneable接口即可;
Java實現原型模式的兩個要點:
1、實現Cloneable接口。在java語言有一個Cloneable接口,它的作用只有一個,就是在運行時通知虛擬機可以安全地在實現了此接口的類上使用clone方法。在java虛擬機中,只有實現了這個接口的類才可以被拷貝,否則在運行時會拋出 CloneNotSupportedException異常。
2、重寫Object類中的clone方法。Java中,所有類的父類都是Object類,Object類中有一個clone方法,作用是返回對象的一個拷貝,但是其作用域protected類型的,一般的類無法調用,因此,Prototype類需要將clone方法的作用域修改為public類型。
Java原型模式代碼結構:
public class PrototypeClass implements Cloneable{ //覆寫父類Object方法 @Override public PrototypeClass clone(){ PrototypeClass prototypeClass = null; try { prototypeClass = (PrototypeClass)super.clone(); } catch (CloneNotSupportedException e) { //異常處理 } return prototypeClass; } }
注意:
Object類的clone方法只會拷貝java中的8中基本類型以及他們的封裝類型,另外還有String類型。對於數組、容器對象、引用對象等都不會拷貝,這就是淺拷貝。如果要實現深拷貝,必須將原型模式中的數組、容器對象、引用對象等另行拷貝。
3、淺拷貝與深拷貝
▶ 淺拷貝(shallow copy)
被復制對象的所有變量都含有與原來的對象相同的值(僅對於簡單的值類型數據),而所有的對其他對象的引用都仍然指向原來的對象。換言之,只負責克隆按值傳遞的數據(比如:基本數據類型、String類型)。
▶ 深拷貝 (deep copy)
被復制對象的所有的變量都含有與原來的對象相同的值,除去那些引用其他對象的變量。那些引用其他對象的變量將指向被復制過的新對象,而不再是原有的那些被引用的對象。換言之,除了淺度克隆要克隆的值外,還負責克隆引用類型的數據,基本上就是被克隆實例所有的屬性的數據都會被克隆出來。
淺拷貝實例↓↓↓:
package com.designpattern.Prototype.shallowcopy; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * 計划 * @author Json<<json1990@foxmail.com>> */ public class Plan implements Cloneable { //計划名稱 private String name; //任務級別 private int level; //開始時間 private Date startdate; //截止時間 private Date enddate; //執行人員 private List<String> executors = new ArrayList<String>(); @Override public Plan clone(){ try { return (Plan) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Date getStartdate() { return startdate; } public void setStartdate(Date startdate) { this.startdate = startdate; } public Date getEnddate() { return enddate; } public void setEnddate(Date enddate) { this.enddate = enddate; } public List<String> getExecutors() { return executors; } public void setExecutors(List<String> executors) { this.executors = executors; } public int getLevel() { return level; } public void setLevel(int level) { this.level = level; } @Override public String toString() { return "[name=" + name + ", level=" + level + ", startdate=" + startdate + ", enddate=" + enddate + ", executors=" + executors + "]"; } }
測試:
package com.designpattern.Prototype.shallowcopy; import java.util.ArrayList; import java.util.List; import com.designpattern.utils.DateFormatUtil; /** * 測試 * @author Json<<json1990@foxmail.com>> */ public class Client { public static void main(String[] args) { List<String> executors = new ArrayList<String>(); executors.add("張三"); executors.add("李四"); Plan plan = new Plan(); plan.setName("重構前端登錄界面"); plan.setLevel(1); plan.setStartdate(DateFormatUtil.stringToDate(DateFormatUtil.YYYYMMDD,"2017-08-07")); plan.setEnddate(DateFormatUtil.stringToDate(DateFormatUtil.YYYYMMDD,"2017-08-09")); plan.setExecutors(executors); Plan plan2 = plan.clone(); plan2.setName("后端接口改造"); plan2.setLevel(2); plan2.setStartdate(DateFormatUtil.stringToDate(DateFormatUtil.YYYYMMDD,"2017-08-10")); plan2.setEnddate(DateFormatUtil.stringToDate(DateFormatUtil.YYYYMMDD,"2017-08-12")); System.out.println("地址是否一樣?"+(plan == plan2)); System.out.println("plan.getName() == plan2.getName() "+(plan.getName() == plan2.getName())); System.out.println("plan.getLevel() == plan2.getLevel() "+(plan.getLevel() == plan2.getLevel())); System.out.println("plan.getStartdate() == plan2.getStartdate() "+(plan.getStartdate() == plan2.getStartdate())); System.out.println("plan.getEnddate() == plan2.getEnddate() "+(plan.getEnddate() == plan2.getEnddate())); System.out.println("plan.getExecutors() == plan2.getExecutors() "+(plan.getExecutors() == plan2.getExecutors())); System.out.println("plan:"+plan.toString()); System.out.println("plan2:"+plan2.toString()); //plan任務比較重,在給plan添加一個人 executors.add("王五"); plan.setExecutors(executors); System.out.println(); System.out.println("地址是否一樣?"+(plan == plan2)); System.out.println("plan.getName() == plan2.getName() "+(plan.getName() == plan2.getName())); System.out.println("plan.getLevel() == plan2.getLevel() "+(plan.getLevel() == plan2.getLevel())); System.out.println("plan.getStartdate() == plan2.getStartdate() "+(plan.getStartdate() == plan2.getStartdate())); System.out.println("plan.getEnddate() == plan2.getEnddate() "+(plan.getEnddate() == plan2.getEnddate())); System.out.println("plan.getExecutors() == plan2.getExecutors() "+(plan.getExecutors() == plan2.getExecutors())); System.out.println("plan:"+plan.toString()); System.out.println("plan2:"+plan2.toString()); } }
結果:
地址是否一樣?false
plan.getName() == plan2.getName() false
plan.getLevel() == plan2.getLevel() false
plan.getStartdate() == plan2.getStartdate() false
plan.getEnddate() == plan2.getEnddate() false
plan.getExecutors() == plan2.getExecutors() true
plan:[name=重構前端登錄界面, level=1, startdate=Mon Aug 07 00:00:00 CST 2017, enddate=Wed Aug 09 00:00:00 CST 2017, executors=[張三, 李四]]
plan2:[name=后端接口改造, level=2, startdate=Thu Aug 10 00:00:00 CST 2017, enddate=Sat Aug 12 00:00:00 CST 2017, executors=[張三, 李四]]
地址是否一樣?false
plan.getName() == plan2.getName() false
plan.getLevel() == plan2.getLevel() false
plan.getStartdate() == plan2.getStartdate() false
plan.getEnddate() == plan2.getEnddate() false
plan.getExecutors() == plan2.getExecutors() true
plan:[name=重構前端登錄界面, level=1, startdate=Mon Aug 07 00:00:00 CST 2017, enddate=Wed Aug 09 00:00:00 CST 2017, executors=[張三, 李四, 王五]]
plan2:[name=后端接口改造, level=2, startdate=Thu Aug 10 00:00:00 CST 2017, enddate=Sat Aug 12 00:00:00 CST 2017, executors=[張三, 李四, 王五]]
通過上面的實例,大家發現沒有,對於引用類型List,修改plan的List,plan2也隨着變化了,這就是淺拷貝帶來的問題;
深拷貝實例(修改淺拷貝實例)↓↓↓:
package com.designpattern.Prototype.deepcopy; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * 計划 * @author Json<<json1990@foxmail.com>> */ public class Plan implements Cloneable { //計划名稱 private String name; //任務級別 private int level; //開始時間 private Date startdate; //截止時間 private Date enddate; //執行人員 private List<String> executors = new ArrayList<String>(); @Override public Plan clone(){ try { Plan plan = (Plan) super.clone(); //引用類型的屬性,需要處理 if(this.getExecutors() != null){ List<String> _executors = new ArrayList<String>(); for(String s : this.getExecutors()){ _executors.add(s); } plan.setExecutors(_executors); } return plan; } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Date getStartdate() { return startdate; } public void setStartdate(Date startdate) { this.startdate = startdate; } public Date getEnddate() { return enddate; } public void setEnddate(Date enddate) { this.enddate = enddate; } public List<String> getExecutors() { return executors; } public void setExecutors(List<String> executors) { this.executors = executors; } public int getLevel() { return level; } public void setLevel(int level) { this.level = level; } @Override public String toString() { return "[name=" + name + ", level=" + level + ", startdate=" + startdate + ", enddate=" + enddate + ", executors=" + executors + "]"; } }
測試(沒改動):
package com.designpattern.Prototype.deepcopy; import java.util.ArrayList; import java.util.List; import com.designpattern.utils.DateFormatUtil; /** * 測試 * @author Json<<json1990@foxmail.com>> */ public class Client { public static void main(String[] args) { List<String> executors = new ArrayList<String>(); executors.add("張三"); executors.add("李四"); Plan plan = new Plan(); plan.setName("重構前端登錄界面"); plan.setLevel(1); plan.setStartdate(DateFormatUtil.stringToDate(DateFormatUtil.YYYYMMDD,"2017-08-07")); plan.setEnddate(DateFormatUtil.stringToDate(DateFormatUtil.YYYYMMDD,"2017-08-09")); plan.setExecutors(executors); Plan plan2 = plan.clone(); plan2.setName("后端接口改造"); plan2.setLevel(2); plan2.setStartdate(DateFormatUtil.stringToDate(DateFormatUtil.YYYYMMDD,"2017-08-10")); plan2.setEnddate(DateFormatUtil.stringToDate(DateFormatUtil.YYYYMMDD,"2017-08-12")); System.out.println("地址是否一樣?"+(plan == plan2)); System.out.println("plan.getName() == plan2.getName() "+(plan.getName() == plan2.getName())); System.out.println("plan.getLevel() == plan2.getLevel() "+(plan.getLevel() == plan2.getLevel())); System.out.println("plan.getStartdate() == plan2.getStartdate() "+(plan.getStartdate() == plan2.getStartdate())); System.out.println("plan.getEnddate() == plan2.getEnddate() "+(plan.getEnddate() == plan2.getEnddate())); System.out.println("plan.getExecutors() == plan2.getExecutors() "+(plan.getExecutors() == plan2.getExecutors())); System.out.println("plan:"+plan.toString()); System.out.println("plan2:"+plan2.toString()); //plan任務比較重,在給plan添加一個人 executors.add("王五"); plan.setExecutors(executors); System.out.println(); System.out.println("地址是否一樣?"+(plan == plan2)); System.out.println("plan.getName() == plan2.getName() "+(plan.getName() == plan2.getName())); System.out.println("plan.getLevel() == plan2.getLevel() "+(plan.getLevel() == plan2.getLevel())); System.out.println("plan.getStartdate() == plan2.getStartdate() "+(plan.getStartdate() == plan2.getStartdate())); System.out.println("plan.getEnddate() == plan2.getEnddate() "+(plan.getEnddate() == plan2.getEnddate())); System.out.println("plan.getExecutors() == plan2.getExecutors() "+(plan.getExecutors() == plan2.getExecutors())); System.out.println("plan:"+plan.toString()); System.out.println("plan2:"+plan2.toString()); } }
結果:
地址是否一樣?false
plan.getName() == plan2.getName() false
plan.getLevel() == plan2.getLevel() false
plan.getStartdate() == plan2.getStartdate() false
plan.getEnddate() == plan2.getEnddate() false
plan.getExecutors() == plan2.getExecutors() false
plan:[name=重構前端登錄界面, level=1, startdate=Mon Aug 07 00:00:00 CST 2017, enddate=Wed Aug 09 00:00:00 CST 2017, executors=[張三, 李四]]
plan2:[name=后端接口改造, level=2, startdate=Thu Aug 10 00:00:00 CST 2017, enddate=Sat Aug 12 00:00:00 CST 2017, executors=[張三, 李四]]
地址是否一樣?false
plan.getName() == plan2.getName() false
plan.getLevel() == plan2.getLevel() false
plan.getStartdate() == plan2.getStartdate() false
plan.getEnddate() == plan2.getEnddate() false
plan.getExecutors() == plan2.getExecutors() false
plan:[name=重構前端登錄界面, level=1, startdate=Mon Aug 07 00:00:00 CST 2017, enddate=Wed Aug 09 00:00:00 CST 2017, executors=[張三, 李四, 王五]]
plan2:[name=后端接口改造, level=2, startdate=Thu Aug 10 00:00:00 CST 2017, enddate=Sat Aug 12 00:00:00 CST 2017, executors=[張三, 李四]]
到這里,大家對原型模式的淺拷貝、深拷貝有了一定了解了吧!
4、優缺點
優點:
1、對客戶端隱藏具體的實現類型:原型模式的客戶端,只知道原型接口的類型,並不知道具體的實現類型,從而減少了客戶端對這些具體實現類型的依賴。
2、在運行時動態改變具體的實現類型:原型模式可以在運行期間,由客戶來注冊符合原型接口的實現類型,也可以動態的改變具體的實現類型,看起來接口沒有任何變化,但其實運行的已經是另外一個類實例了。因為克隆一個原型就類似於實例化一個類。
缺點:
深度克隆方法實現會比較困難:原型模式最大的缺點就在於每個原型的子類都必須實現clone的操作,尤其在包含引用類型的對象時,clone方法會比較麻煩,必須要能夠遞歸的讓所有的相關對象都要正確的實現克隆。
5、應用場景
1、一個系統想要獨立於它想要使用的對象時,可以使用原型模式,讓系統只面向接口編程,在系統需要新的對象的時候,可以通過克隆原型來得到;
2、需要實例化的類是在運行時刻動態指定時,可以使用原型模式,通過克隆原型來得到需要的實例;
3、創建新對象成本較大(如初始化需要占用較長的時間,占用太多的CPU資源或網絡資源),新的對象可以通過原型模式對已有對象進行復制來獲得,如果是相似對象,則可以對其成員變量稍作修改。
.......
6、擴展總結
Java中對象的創建:
clone就是復制,在Java中,clone方法被對象調用,所以會復制對象。所謂的復制對象,首先要分配一個和源對象同樣大小的空間,在這個空間中創建一個新的對象。
那么在java語言中,有幾種方式可以創建對象呢?
1、使用new操作符創建一個對象
2、使用clone方法復制一個對象
那么這兩種方式有什么相同和不同呢?
▶ new操作符的本意是分配內存。程序執行到new操作符時,首先去看new操作符后面的類型,因為知道了類型,才能知道要分配多大的內存空間。分配完內存之后,再調用構造函數,填充對象的各個域,這一步叫做對象的初始化,構造方法返回后,一個對象創建完畢,可以把他的引用(地址)發布到外部,在外部就可以使用這個引用操縱這個對象。
▶ clone在第一步是和new相似的,都是分配內存,調用clone方法時,分配的內存和源對象(即調用clone方法的對象)相同,然后再使用原對象中對應的各個域,填充新對象的域,填充完成之后,clone方法返回,一個新的相同的對象被創建,同樣可以把這個新對象的引用發布到外部。
clone 方法需要滿足的幾個原則:
x.clone() != x,兩個對象不是一個;
x.clone().getClass() == x.getClass(),兩個對象的class是同一個;
x.clone().equals(x),兩個應該滿足equals()方法條件;
注意:
Object類的clone方法只會拷貝java中的8中基本類型以及他們的封裝類型,另外還有String類型。對於數組、容器對象、引用對象等都不會拷貝,這就是淺拷貝。如果要實現深拷貝,必須將原型模式中的數組、容器對象、引用對象等另行拷貝。
PS: 要使用clone方法,類的成員變量上不要增加final關鍵字。
PS:深拷貝和淺拷貝建議不要混合使用,特別是是在涉及到類的繼承,父類有多個引用的情況就非常的復雜,建議的方案是深拷貝和淺拷貝分開實現。
PS:java實現真正的深拷貝,可以通過實現Serializable接口用序列化的辦法來實現。
PS:源碼地址 https://github.com/JsonShare/DesignPattern/tree/master
PS:原文地址 http://www.cnblogs.com/JsonShare/p/7300124.html
