原型模式雖然是建立型的模式,但是與工程模式沒有關系,從名字即可看出,該模式的思想就是将一個對象作為原型,對其進行複制、克隆,産生一個和原對象類似的新對象。
一、應用場景
假設有這樣一種情景:如果你正在開發一個銀行管理系統,其中有一個功能是在用戶端檢視某人的賬戶餘額,你采用簡單工廠模式,由AccountFactory負責根據使用者傳入的使用者名建立使用者賬号的對象,然後傳回給用戶端,具體代碼如下:
Client(用戶端):
public class Test {
public static void main(String[] args) {
String name = "laowang";
AccountFactory af = new AccountFactory();
PersonAccount pa = af.getPersonAccount(name);
System.out.println(name+"的賬戶餘額是:"+pa.getAccount());
}
}
AccountFactory(賬号工廠類):
public class AccountFactory {
PersonAccount personAccount = null;
public PersonAccount getPersonAccount(String name){
if(personAccount == null){
personAccount = new PersonAccount();
personAccount.setName(name);
personAccount.setAccount(1000);
personAccount.setAddress("北京");
}
return personAccount;
}
}
PersonAccount(賬号類):
public class PersonAccount {
private int account;//賬戶餘額
private String name;//賬戶名
private String address;//賬戶擁有者位址
public int getAccount() {
return account;
}
public void setAccount(int account) {
this.account = account;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
現在我們來運作下,我們驚喜地發現運作結果為:
laowang的賬戶餘額是:1000
運作成功,沒有任何錯誤,就目前狀态來看,代碼很完美。可是,事情往往沒有你想象的那麼簡單。下面我們來在用戶端高點小破壞,加入,我就是laowang,一檢視賬戶餘額,怎麼隻剩以前了,一定是上個月網購花太多了,怎麼辦呢?靈機一動,有了,我們在用戶端加幾行代碼吧,改動後的用戶端如下:
public class Test {
public static void main(String[] args) {
String name = "laowang";
AccountFactory af = new AccountFactory();
PersonAccount pa = af.getPersonAccount(name);
System.out.println(name+"的賬戶餘額是:"+pa.getAccount());
pa.setAccount(100000);//添加的代碼
//重新查詢我的賬戶餘額
pa = af.getPersonAccount(name);
System.out.println(name+"的賬戶餘額(改過後的)是:"+pa.getAccount());
}
}
我們再來運作一下,發現結果如下:
laowang的賬戶餘額是:1000
laowang的賬戶餘額(改過後的)是:100000
隻改動一行,就變成了10萬塊。但是,如果你是銀行你肯定忍不了這樣的事情發生,那麼究竟怎麼避免和解決這種漏洞呢?之是以會産生這樣的錯誤,是因為我們傳回給用戶端的PersonAccount對象是引用類型的資料,用戶端的改變會導緻真實的資料改變。我們知道如果能夠像值傳遞一樣,不傳遞位址(引用),就可以避免這樣的錯誤。那麼究竟如何做呢?這裡,就要用到我們的神奇克隆術——Java深、淺Clone。
二、Java淺Clone
我們首先來看一下,使用Java的Clone機制如何改善我們的系統,具體的代碼改變如下:
PsersionAccount(賬号類):
public class PersonAccount implements Cloneable{
private int account;//賬戶餘額
private String name;//賬戶名
private String address;//賬戶擁有者位址
//增加Clone方法
public Object clone(){
PersonAccount personAccount = null;
try{
personAccount = (PersonAccount)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return personAccount;
}
//省略了get、set方法
}
AccountFactory(賬号工廠類):
public class AccountFactory {
PersonAccount personAccount = null;
public PersonAccount getPersonAccount(String name){
if(personAccount == null){
personAccount = new PersonAccount();
personAccount.setName(name);
personAccount.setAccount(1000);
personAccount.setAddress("北京");
}
return (PersonAccount) personAccount.clone();//改動的地方
}
}
上面兩個類中,PersonAccount類增加了一個方法,實作了一個接口。AccountFactory中的傳回語句發生了變動。改動很簡單,我們再來運作一下我們的程式,我們發現,現在輸出為:
laowang的賬戶餘額是:1000
laowang的賬戶餘額(改過後的)是:1000
我們看到:此時,使用者是無法通過用戶端來改變使用者的賬戶餘額的,我們已經成功解決了第一個麻煩。下面,我們就來解釋下,我們到底是如何解決的。
首先,我們來看一下改變比較大的PersonAccount類:我們可以看到PersonAccount類implements了一個Cloneable接口,那麼這個接口究竟做了什麼呢?我們來看一下Java源碼,如下:
/*
* Copyright (c) 1995, 2004, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package java.lang;
/**
* A class implements the <code>Cloneable</code> interface to
* indicate to the {@link java.lang.Object#clone()} method that it
* is legal for that method to make a
* field-for-field copy of instances of that class.
* <p>
* Invoking Object's clone method on an instance that does not implement the
* <code>Cloneable</code> interface results in the exception
* <code>CloneNotSupportedException</code> being thrown.
* <p>
* By convention, classes that implement this interface should override
* <tt>Object.clone</tt> (which is protected) with a public method.
* See {@link java.lang.Object#clone()} for details on overriding this
* method.
* <p>
* Note that this interface does <i>not</i> contain the <tt>clone</tt> method.
* Therefore, it is not possible to clone an object merely by virtue of the
* fact that it implements this interface. Even if the clone method is invoked
* reflectively, there is no guarantee that it will succeed.
*
* @author unascribed
* @see java.lang.CloneNotSupportedException
* @see java.lang.Object#clone()
* @since JDK1.0
*/
public interface Cloneable {
}
我們從上面的源碼看到,Cloneable接口中其實一個方法也沒有,其實這個接口隻是一個标志,聲明implements Cloneable的類隻是表示這個類實作了Clone方法,我們知道所有的類都預設繼承了java.lang.Object類,是以,我們去看看這個父類的clone方法是如何實作的。看一下java源碼,如下:
/* @return a clone of this instance.
* @exception CloneNotSupportedException if the object's class does not
* support the {@code Cloneable} interface. Subclasses
* that override the {@code clone} method can also
* throw this exception to indicate that an instance cannot
* be cloned.
* @see java.lang.Cloneable
*/
protected native Object clone() throws CloneNotSupportedException;
從上圖,我們可以獲得三個問題的答案:
第一個:我們看到clone()方法的通路權限為protected,這就是我們為什麼要在子類中覆寫重寫父類clone方法,并且将通路修飾符改為public的原因,因為隻有這樣才能實作包外非子類的自由方法,當然了,如果你想要自己的方法限制為包類或子類通路的話,可以不改變修飾符。
第二個:我們看到close()方法依舊沒有方法體,但是我們發現clone()為native方法,native是什麼意思呢?native方法表示這個方法是有非java語言來編寫的,大多數為本地方法,大多數與作業系統等底層實作有關,當然了,效率也就比普通的方法高很多。其實,這也就是我們為什麼要調用父類clone()方法,而不是自己new一個對象,然後進行一次按複制來完成一個對象的拷貝。調用父類的clone方法不僅效率較高,而且操作簡單,特别是在對象屬性較多的情況下,更加明顯。
第三個:我們看到該方法會抛出一個異常CloneNotSuppotedException,我們看到上面的方法注釋中說明了,如果我們沒有生命Implements Cloneable接口的話,就會抛出這個異常,其實,這也是我們為什麼要聲明Implements Cloneable接口,即使它其實一個方法也沒有。
到這裡,我們基本解決并解釋了第一個問題,如果你覺得這裡就萬事大吉了。
其實事情往往沒有你想象的name簡單。
我們再來搞點破壞。我們知道,正常情況下,我們會有一個單獨的User類來負責管理存儲使用者資訊,其實PersonAccount類中,應該持有一個User類的對象,通過這個對象擷取使用者資訊,而不是直接将name,address等使用者資訊直接存儲在PersonAccount類中,特别是那些和Account無關的使用者資訊等。那麼,我們來改造下我們的系統:
User類:
public class User {
private String name;//賬戶名
private String address;//賬戶擁有者位址
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
PersonAccount類:主要增加了User屬性,删除name、address屬性
public class PersonAccount implements Cloneable{
private int account;//賬戶餘額
private User user;
//增加Clone方法
public Object clone(){
PersonAccount personAccount = null;
try{
personAccount = (PersonAccount)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return personAccount;
}
public int getAccount() {
return account;
}
public void setAccount(int account) {
this.account = account;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
AccountFactory類:改變name指派為生成User對象
public class AccountFactory {
PersonAccount personAccount = null;
public PersonAccount getPersonAccount(String name){
if(personAccount == null){
personAccount = new PersonAccount();
personAccount.setAccount(1000);
//生成User對象
User user = new User();
user.setName(name);
user.setAddress(name+"的家庭位址");
personAccount.setUser(user);
}
//改動的地方
return (PersonAccount) personAccount.clone();
}
}
用戶端代碼:
public class Test {
public static void main(String[] args) {
String name = "laowang";
AccountFactory af = new AccountFactory();
PersonAccount pa = af.getPersonAccount(name);
//修改後的代碼
User user = pa.getUser();
System.out.println(user.getName()+":"+user.getAddress());
System.out.println("賬戶餘額:"+pa.getAccount());
System.out.println("**********************************");
//嘗試修改賬戶餘額
pa.setAccount(100000);
//再調用查詢功能
pa = af.getPersonAccount(name);
User user1 = pa.getUser();
System.out.println(user1.getName()+":"+user1.getAddress());
System.out.println("賬戶餘額:"+pa.getAccount());
System.out.println("**********************************");
//嘗試修改使用者資訊類User
user1.setName("我自己");
user1.setAddress("我自己的住址");
//再調用查詢功能
pa = af.getPersonAccount(name);
User user2 = pa.getUser();
System.out.println(user2.getName()+":"+user2.getAddress());
System.out.println("賬戶餘額:"+pa.getAccount());
System.out.println("**********************************");
}
}
以上的全部代碼主要展示了我們增加User類之後的系統,我們來運作一下,發現結果如下:
laowang:laowang的家庭位址
賬戶餘額:1000
**********************************
laowang:laowang的家庭位址
賬戶餘額:1000
**********************************
我自己:我自己的住址
賬戶餘額:1000
**********************************
從上面的結果可以看出,我們加入User類之後,依舊能夠防止對賬戶餘額(Account)的修改,但是,這種情況下,竟然可以修改使用者資訊(User),将别人的賬戶改為自己的賬戶,據為己有,這肯定是不行的。
之是以會産生上面的結果,是因為我們這裡采用的是Java的淺Clone,如果我們采用Java的深Clone的話,會在一定程度上避免上面的錯誤産生。那麼究竟上面是Java淺Clone,什麼是Java深Clone,它們又有怎樣的差別呢?我們繼續往下看。
三、Java深Clone
在解釋Java深Clone、淺Clone的差別淺,我們先來解釋下Java中Clone方法的底層實作機制。由于clone方法類型為native,我們并不能看到它的具體代碼實作,那麼它的底層究竟是如果實作的呢?
其實,是這樣的,在執行clone操作的時候,底層會申請出一塊和原來對象所占用空間一樣大小的存儲空間,然後将原來對象所占空間的所有資料都原樣拷貝到新申請到的空間,這樣就獲得了一個和原來一模一樣的對象。但是,我們需要注意的是,在拷貝過程中,值類型的資料,當然沒有問題,比如int型,String型等其他的基本資料類型。但,對于引用類型的資料,如對象呢。在原來的位址空間中存儲的就是一個指向真實對象的引用,拷貝到新的位址空間之後,引用還是指向同一個真實對象。如下圖:
那麼現在我們對clone得到的對象中的Account等值類型資料進行更改的時候,并不會影響原有的對象中的資料,但是,當我們對clone得到的對象中的user等引用類型資料進行更改的時候,因為指向的是同一個真實對象,那麼就一定會影響到原有的對象中的資料,這就是所謂的淺clone。
那麼,我們如何避免這種情況呢,那就是在對PsersonAccount對象進行clone操作時,對其中的user鍍錫等所有引用類型的資料也同樣進行一次clone操作,當然了,這樣操作的前提是user類能夠像personAccount類一樣實作cloneable接口,并重寫父類的clone方法,這就是深clone。
下面,我們來看一下,就目前我們的狀況:存在簡單嵌套的情況下,我們應該如何進行深度Clone呢。
User類:
public class User implements Cloneable{
private String name;//賬戶名
private String address;//賬戶擁有者位址
public Object clone(){
User user = null;
try{
user = (User)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return user;
}
//省略get、set方法
}
PersonAccount類:
public class PersonAccount implements Cloneable{
private int account;//賬戶餘額
private User user;
//增加Clone方法
public Object clone(){
PersonAccount personAccount = null;
try{
personAccount = (PersonAccount)super.clone();
User user = (User)personAccount.getUser().clone();
personAccount.setUser(user);
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return personAccount;
}
//省略get、seet方法
}
此時,深clone的操作情況如下:
此時,我們再來運作一下用戶端的代碼,我們發現,運作結果如下:
laowang:laowang的家庭位址
賬戶餘額:1000
**********************************
laowang:laowang的家庭位址
賬戶餘額:1000
**********************************
laowang:laowang的家庭位址
賬戶餘額:1000
**********************************
我們發現,采用深clone之後,無論是值類型資料還是引用類型資料,都無法在用戶端進行修改。到這裡,我們的系統比一開始的原始系統要安全健壯多了。
當然了,如果user類中還持有其他引用資料類型的資料,那麼對象這些資料,也必須采用同樣的深clone操作,也就是說存在于引用鍊中的所有引用類型資料,都必須進行深clone操作。你可能會說,這要是很多層引用嵌套,豈不是會呈現爆炸式地複雜度增加,不可否認,确實是這樣的。也正是因為這個原因,是以一般在引用嵌套層數較少的情況下才使用深clone,在嵌套層數過多時,往往會導緻複雜度過高而無法使用深度clone。此時,也許會因為在複雜度和深度clone之間的權衡中,做出一種“不倫不類”的做法,那就是一部分引用類型的資料使用clone操作,一部分引用類型資料直接使用new并以此按項複制的方式進行手動clone。當然,不用說,也知道這是一種不太好的做法。
那麼我們應該如何應對嵌套層數較多,資料資訊内容較複雜的對象的clone操作呢?也許采用序列化的方式,是一個不錯的選擇,那麼如何通過序列化方式進行相關操作呢?
四、序列化實作深克隆
再次更改系統代碼
PersonAccount類:
public class PersonAccount implements Cloneable{
private int account;//賬戶餘額
private User user;
//增加Clone方法
public Object clone(){
PersonAccount personAccount = null;
try{
//輸出流
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bout);
oos.writeObject(this);
oos.close();
//輸入流
ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bin);
personAccount = (PersonAccount) ois.readObject();
ois.close();
}catch(Exception e){
e.printStackTrace();
}
return personAccount;
}
//省略get、seet方法
}
接下來我們看下運作結果:
laowang:laowang的家庭位址
賬戶餘額:1000
**********************************
laowang:laowang的家庭位址
賬戶餘額:1000
**********************************
laowang:laowang的家庭位址
賬戶餘額:1000
**********************************
到這裡克隆就講完啦~整理來自于https://blog.csdn.net/qiumengchen12/article/details/45022919