天天看點

23種設計模式——原型模式一、應用場景二、Java淺Clone三、Java深Clone四、序列化實作深克隆

原型模式雖然是建立型的模式,但是與工程模式沒有關系,從名字即可看出,該模式的思想就是将一個對象作為原型,對其進行複制、克隆,産生一個和原對象類似的新對象。

一、應用場景

假設有這樣一種情景:如果你正在開發一個銀行管理系統,其中有一個功能是在用戶端檢視某人的賬戶餘額,你采用簡單工廠模式,由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型等其他的基本資料類型。但,對于引用類型的資料,如對象呢。在原來的位址空間中存儲的就是一個指向真實對象的引用,拷貝到新的位址空間之後,引用還是指向同一個真實對象。如下圖:

23種設計模式——原型模式一、應用場景二、Java淺Clone三、Java深Clone四、序列化實作深克隆

那麼現在我們對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的操作情況如下:

23種設計模式——原型模式一、應用場景二、Java淺Clone三、Java深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

繼續閱讀