設計模式(java)- 原型模式
1. 簡介
原型模式又稱克隆模式,它允許一個物件可以創建出另一個與自身型別相同的物件。客戶不需要知道建立的細節,就可以獲取到需要的複製品。
在java中通過繼承Cloneable的介面實現原型模式,在c++中則可以通過過載賦值函式進行實現。
在實現原型模式的時候,我們會根據實際情況和業務模型對其做不同的實現,一般分為淺拷貝和深拷貝兩種方式來對原物件進行復制,從而創建出一個新的物件。
2. 類圖
原型模式的設計類圖很簡單,它需要一個原型類,繼承Cloneable,通過實現介面中的clone方法來實現對原型的物件建立。如下所示:
3. 例項
首先,我們建立一個原型發票類,然後實現發票的clone方法,從而複製出另一張發票物件。
3.1 原型類
3.1.1 發票類:
class EInvoice implements Cloneable {
private String strName;
private String strCompanyName;
private String strId;
private String strDate;
private float fAmount;
public EInvoice clone() {
EInvoice invoice = null;
try {
invoice = (EInvoice)super.clone();
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return invoice;
}
public String getName() {
return this.strName;
}
public void setName(String strName) {
this.strName = strName;
}
}
3.1.2 客戶呼叫:
EInvoice invoice= new EInvoice();
invoice. setName("張三");
EInvoice invoiceClone = invoice.clone();
System.out.println(invoiceClone.getName());
如上,發票類繼承了Cloneable介面,然後通過實現clone方法來實現對自身型別的物件建立。這裡直接呼叫父類的clone方法,進行向下型別轉換完成發票物件的建立。父類的克隆方法屬於淺拷貝的方式。
3.2 淺拷貝
淺拷貝,顧名思義,就是拷貝一些面子上的東西,而沒有拷貝實質的裡子。即當一個物件內部足夠複雜時,例如內部有其他型別的例項A(引用資料型別),如果進行淺拷貝的話,實際上只是拷貝了這個物件的引用,即拷貝出來的物件中的A例項與原物件中A例項指向了同一塊記憶體,如果我們改變了原物件中例項A的內容,拷貝後的物件中的例項A的內容也會發生同樣的變化,這點在C++中通常是指標的拷貝,而非指標所指向的記憶體拷貝。
3.2.1 發票集合類
class EInvoiceCollection implements Cloneable {
private ArrayList<EInvoice> invoiceList;
public EInvoiceCollection() {
invoiceList = new ArrayList<EInvoice>();
}
public void addInvoice(EInvoice invoice) {
invoiceList.add(invoice);
}
public EInvoiceCollection clone() {
EInvoiceCollection invoiceCollection = null;
try {
invoiceCollection = (EInvoiceCollection)super.clone();
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return invoiceCollection;
}
public void printAllInvoiceName() {
if (this.invoiceList == null) {
return;
}
int nSize = this.invoiceList.size();
for (int i = 0; i < nSize; i++) {
String strName = this.invoiceList.get(i).getName();
System.out.println(strName);
}
}
}
如上,是一個發票集合類,內部含有一個ArrayList集合,同樣對其進行了繼承Cloneable介面,然後實現clone方法。通過客戶程式碼呼叫和輸出,驗證一下其是否為淺拷貝:
public class EPrototype {
public static void main(String[] args) {
// TODO Auto-generated method stub
EInvoice invoice= new EInvoice();
invoice.setName("張三");
EInvoice invoiceClone = invoice.clone();
System.out.println(invoiceClone.getName());
EInvoiceCollection invoiceCollection = new EInvoiceCollection();
for (int i = 0; i < 3; i++) {
invoiceCollection.addInvoice(invoice.clone());
}
EInvoiceCollection invoiceCollClone = invoiceCollection.clone();
invoiceClone.setName("李四");
invoiceCollClone.addInvoice(invoiceClone);
System.out.println("原型發票集合:");
invoiceCollection.printAllInvoiceName();
System.out.println("淺拷貝後發票集合:");
invoiceCollClone.printAllInvoiceName();
}
}
輸出結果如下:
張三
原型發票集合:
張三
張三
張三
李四
淺拷貝後發票集合:
張三
張三
張三
李四
根據輸出結果,可以看到,淺拷貝和原型的發票集合的結果是一致的。也就是對拷貝後的發票集合新增一個發票,原型也跟著新增了一個發票。這就是淺拷貝,兩個物件中的ArrayList是同一個,拷貝出來的物件擁有ArrayList的引用,所以改變拷貝出來的物件中的ArrayList,其實就是改變原型中的ArrayList。在一些特殊的業務場景中,淺拷貝是很有用的,但使用者一定要知道其是個淺拷貝,專門服務於業務而做的設計。
3.3 深拷貝
有淺拷貝,對應的就有深拷貝,深拷貝其實就是將引用型別所指向的內容拷貝到另一個物件中,這兩個物件除了型別一致外,他們所佔用的記憶體地址是不一樣的。對兩者中的任意一個修改都不會影響到另外一個。
一般的,如果一個類很複雜,內部有很多引用型別的成員,我們會禁用深拷貝。但是對於類不是很複雜,內部有少量的引用型別的成員,如果業務有需求,可以提供深拷貝的方法,這些都是根據業務需求來變化。現提供兩種深拷貝的方法。
3.3.1 窮舉呼叫成員的clone方法
// Deep cloning
class EInvoiceCollection implements Cloneable {
ArrayList<EInvoice> invoiceList;
public EInvoiceCollection() {
invoiceList = new ArrayList<EInvoice>();
}
public void addInvoice(EInvoice invoice) {
invoiceList.add(invoice);
}
public EInvoiceCollection clone() {
EInvoiceCollection invoiceCollection = null;
try {
invoiceCollection = (EInvoiceCollection)super.clone();
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
invoiceCollection.invoiceList = (ArrayList<EInvoice>) this.invoiceList.clone();
return invoiceCollection;
}
public void printAllInvoiceName() {
if (this.invoiceList == null) {
return;
}
int nSize = this.invoiceList.size();
for (int i = 0; i < nSize; i++) {
String strName = this.invoiceList.get(i).getName();
System.out.println(strName);
}
}
}
還是呼叫之前的客戶程式碼,輸出結果:
張三
原型發票集合:
張三
張三
張三
淺拷貝後發票集合:
張三
張三
張三
李四
如果內部的引用型別的物件類本身也提供了深拷貝的方法,就可以在聚合類中一個個的呼叫成員的Clone方法實現深拷貝,顯然這種方法是比較麻煩的,有些引用型別成員沒有clone方法就不行了,且引用型別成員多的話,也會容易出錯,維護麻煩。
3.3.2 序列化物件
interface ECloneable extends Cloneable, Serializable {
}
class EInvoice implements ECloneable {
private String strName;
private String strCompanyName;
private String strId;
private String strDate;
private float fAmount;
public EInvoice clone() {
EInvoice invoice = null;
try {
invoice = (EInvoice)super.clone();
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return invoice;
}
public String getName() {
return this.strName;
}
public void setName(String strName) {
this.strName = strName;
}
}
class EInvoiceCollection implements ECloneable {
ArrayList<EInvoice> invoiceList;
public EInvoiceCollection() {
invoiceList = new ArrayList<EInvoice>();
}
public void addInvoice(EInvoice invoice) {
invoiceList.add(invoice);
}
public EInvoiceCollection clone() {
EInvoiceCollection invoiceCollection = null;
ByteArrayOutputStream byteOS = new ByteArrayOutputStream();
ObjectOutputStream objectOS;
try {
objectOS = new ObjectOutputStream(byteOS);
objectOS.writeObject(this);
} catch (IOException e) {
e.printStackTrace();
}
ByteArrayInputStream byteIS = new ByteArrayInputStream(byteOS.toByteArray());
ObjectInputStream objectIS;
try {
objectIS = new ObjectInputStream(byteIS);
invoiceCollection = (EInvoiceCollection) objectIS.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return invoiceCollection;
}
public void printAllInvoiceName() {
if (this.invoiceList == null) {
return;
}
int nSize = this.invoiceList.size();
for (int i = 0; i < nSize; i++) {
String strName = this.invoiceList.get(i).getName();
System.out.println(strName);
}
}
}
客戶程式碼仍舊不變,輸出結果如下:
張三
原型發票集合:
張三
張三
張三
淺拷貝後發票集合:
張三
張三
張三
李四
如上,通過繼承Serializable介面在clone方法中使用位元組流將物件先寫入物件輸出流,這一過程其實就是拷貝原型的堆記憶體中的內容到另個記憶體中,然後再轉換回原型型別,實現深拷貝。
4. 總結
原型模式相對比較簡單,理解了淺拷貝和深拷貝就會很好的利用原型模式,如果你對c++中的淺拷貝和深拷貝熟悉些,那麼對java中的淺拷貝和深拷貝就不難理解了。