1. 程式人生 > >設計模式:原型模式介紹 && 原型模式的深拷貝問題

設計模式:原型模式介紹 && 原型模式的深拷貝問題


# 0、背景
克隆羊問題:有一個羊,是一個類,有對應的屬性,要求建立完全一樣的10只羊出來。 那麼實現起來很簡單,我們先寫出羊的類: ```java public class Sheep { private String name; private int age; private String color; //下面寫上對應的get和set方法,以及對應的構造器 } ``` 然後,建立10只一樣的羊,就在客戶端寫一個程式碼建立: ```java //原始羊 Sheep sheep = new Sheep("tom",1,"白色"); //克隆羊 Sheep sheep1 = new Sheep(sheep.getName(),sheep.getAge(),sheep.getColor()); ``` sheep1 是克隆的第一隻羊,接著就可以複製十遍這個程式碼,然後命名不同的羊,以原始sheep為模板進行克隆。 這種方法的弊端: 1. 建立新物件,**總是需要重新獲取原始物件的屬性值**,效率低; 2. 總是需要重新初始化物件,而不是動態獲取物件執行時的狀態,不靈活。(什麼意思呢,比如原始的 Sheep 有一項要修改,那麼剩下的以它為範本的,必然要重新初始化)
# 一、原型模式
1. 原型模式指的是,用原型例項指定建立物件的種類,並通過拷貝這些原型,建立新的物件; 2. 原型模式是一種建立型設計模式,允許一個物件再建立另一個可以定製的物件,無需知道如何建立的細節; 3. 工作原理是:發動建立的這個物件,請求原型物件,讓**原型物件來自己實施建立**,就是**原型物件.clone()**。 如下類圖所示:
其中,Prototype 是一個原型介面,在這裡面把克隆自己的方法宣告出來; ConcreteProtype 可以是一系列的原型類,實現具體操作。 ### java 的 Object 類是所有類的根類,Object提供了一個 clone() 方法,該方法可以將一個物件複製一份,但是想要實現 clone 的 java 類必須要實現 Cloneable 介面,實現了之後這個類就具有複製的能力。 對於克隆羊問題,我們來利用原型設計模式進行改進: 讓Sheep類,實現 Cloneable 介面: ```java public class Sheep implements Cloneable{ private String name; private int age; private String color; //getters&&setters&&constructors @Override protected Object clone() { Sheep sheep = null; try { sheep = (Sheep)super.clone();//使用預設Object的clone方法來完成 } catch (CloneNotSupportedException e) { System.out.println(e.getMessage()); } return sheep; } } ``` 現在的 Sheep 類就是一個具體的原型實現類了,我們想要克隆的時候,客戶端呼叫可以這樣: ```java Sheep sheep1 = (Sheep) sheep.clone(); Sheep sheep2 = (Sheep) sheep.clone(); //。。。。。類似 ``` 這種做法就是原型設計模式。 >
(spring框架裡,通過bean標籤配置類的scope為prototype,就是用的原型模式)
# 二、原型模式的淺拷貝、深拷貝問題
使用上面所說的原型模式,按理說是複製出了一模一樣的物件。 但我們做一個嘗試,如果 **sheep 類裡的成員變數有一個是物件,而不是基礎型別呢**? ```java private Sheep friend; ``` 然後我們建立、再克隆: ```java Sheep sheep = new Sheep("tom",1,"白色");//原始羊 sheep.setFriend(new Sheep("jack",2,"黑色")); Sheep sheep1 = (Sheep) sheep.clone(); Sheep sheep2 = (Sheep) sheep.clone(); Sheep sheep3 = (Sheep) sheep.clone(); ``` 重寫一下 Sheep 類的 toString 方法,輸出資訊和對應的屬性的 hashcode 後會發現: ```java Sheep{name='tom', age=1, color='白色', friend=488970385} Sheep{name='tom', age=1, color='白色', friend=488970385} Sheep{name='tom', age=1, color='白色', friend=488970385} ``` friend 的 hashCode 值都一樣,也就是克隆的類的 friend 屬性**其實沒有被複制,而是指向了同一個物件。** 這就叫淺拷貝(shallow copy): 1. 對於資料型別是基本資料型別的成員變數,淺拷貝會直接進行值傳遞,也就是複製一份給新物件; 2. 對於資料型別是引用資料型別的成員變數,淺拷貝會進行引用傳遞,也就是隻是將地址指標複製一份給新物件,實際上覆制前和複製後的內容都指向同一個例項。這種情況,顯然在一個物件裡修改成員變數,會影響到另一個物件的成員變數值(因為修改的都是同一個) 3. 預設的 clone() 方法就是淺拷貝。
在原始碼裡也說明了,這個方法是**shallow copy 而不是 deep copy**。 在實際開發中,往往是希望克隆的過程中,如果類的成員是引用型別,也能完全克隆一份,也就是所謂的**深拷貝**。 深拷貝(Deep Copy): 1. 複製物件的所有基本資料型別成員變數值; 2. 為所有 **引用資料型別** 的成員變數申請儲存空間,並且也**複製每個 引用資料型別的成員變數 引用的 所有物件**,一直到該物件可達的所有物件; 深拷貝的實現方式,需要通過重寫 clone 方法,或者通過物件的序列化。 下面來實現一下。
## 2.1 通過重寫 clone 方法深拷貝 ```java /* 被拷貝的類引用的類,此類的clone用預設的clone即可 */ public class CloneTarget implements Cloneable { private static final long serialVersionUID = 1L; private String cloneName; private String cloneClass; public CloneTarget(String cloneName, String cloneClass) { this.cloneName = cloneName; this.cloneClass = cloneClass; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } } ``` ```java /* 原型類,其中有成員是引用型別,因此clone方法要重寫達到深拷貝 */ public class Prototype implements Cloneable { public String name; public CloneTarget cloneTarget; public Prototype() { super(); } @Override protected Object clone() throws CloneNotSupportedException { Object o = null; //用了淺拷貝,基本資料克隆完成,但是cloneTarget指向的還是原來的物件 o = super.clone(); //單獨處理引用型別 Prototype target = (Prototype) o; target.cloneTarget = (CloneTarget)cloneTarget.clone(); return target; } } ``` 這樣的話,新建一個原型Prototype的物件後,對他進行克隆,得到的裡面的 CloneTarget 成員也是深拷貝的兩個不一樣的物件了。 但是這種方法本質上是相當於 **套娃** ,因為都要單獨處理重寫 clone 方法,所以有些麻煩。
## 2.2 通過物件的序列化 在 Prototype 裡直接 **使用序列化+反序列化**,達到對這個物件整體的一個複製。 另外注意,序列化和反序列化,必須實現 Serializable 介面,所以 implements 後面不止要有 Cloneable,還有Serializable。 ```java //利用序列化實現深拷貝 public Object deepClone(){ ByteArrayOutputStream bos = null; ObjectOutputStream oos = null; ByteArrayInputStream bis = null; ObjectInputStream ois = null; try { bos = new ByteArrayOutputStream(); oos = new ObjectOutputStream(bos); oos.writeObject(this); //反序列化 bis = new ByteArrayInputStream(bos.toByteArray()); ois = new ObjectInputStream(bis); Prototype copy = (Prototype) ois.readObject(); return copy; } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); }finally { try { bos.close(); oos.close(); bis.close(); ois.close(); } catch (IOException e) { e.printStackTrace(); } } return null; } ``` 然後我們想要克隆的時候,直接呼叫這個 deepClone 方法就可以達到目的。 忽視掉裡面的 try - catch 之類的程式碼,其實核心部分就是用到序列化和反序列化的總共 4 個物件。這種方法是推薦的,因為實現起來更加容易。 序列化反序列化達到深拷貝目的的原理: * ObjectOutputStream 將 Java 物件的基本資料型別和圖形寫入 OutputStream,但是隻能將支援 java.io.Serializable 介面的物件寫入流中。 **在這裡,我們採用的OutputStream是ByteArrayOutputStream——位元組陣列輸出流,通過建立的ObjectOutputStream的writeObject方法,把物件寫進了這個位元組陣列輸出流。** * 相對應的,ObjectInputStream反序列化原始資料,恢復以前序列化的那些物件。 **在這裡,把位元組陣列重新構造成一個ByteArrayInputStream——位元組陣列輸入流,通過ObjectInputStream的readObject方法,把輸入流重新構造成一個物件。** 結合上面的程式碼再看看: ```java bos = new ByteArrayOutputStream(); oos = new ObjectOutputStream(bos);//寫入指定的OutputStream oos.writeObject(this);//把物件寫入到輸出流中,整個物件,this bis = new ByteArrayInputStream(bos.toByteArray()); ois = new ObjectInputStream(bis);//讀取指定的InputStream Prototype copy = (Prototype) ois.readObject();//從輸入流中讀取一個物件 return copy; ```
# 三、總結
**原型模式:** 1. 當需要建立一個新的物件的內容比較複雜的時候,可以利用原型模式來簡化建立的過程,同時能夠提高效率。 2. 因為這樣不用重新初始化物件,而是動態地獲得物件執行時的狀態,如果原始的物件內部發生變化,其他克隆物件也會發生相應變化,無需一 一修改。 3. 實現深拷貝的方法要注意。 **缺點:** **每一個類都需要一個克隆方法**,對於全新的類來說不是問題,但是如果是用已有的類進行改造,那麼可能會因為要修改原始碼而違背 OCP