0、背景
克隆羊問題:有一個羊,是一個類,有對應的屬性,要求建立完全一樣的10只羊出來。
那麼實現起來很簡單,我們先寫出羊的類:
public class Sheep {
private String name;
private int age;
private String color;
//下面寫上對應的get和set方法,以及對應的構造器
}
然後,建立10只一樣的羊,就在客戶端寫一個程式碼建立:
//原始羊
Sheep sheep = new Sheep("tom",1,"白色");
//克隆羊
Sheep sheep1 = new Sheep(sheep.getName(),sheep.getAge(),sheep.getColor());
sheep1 是克隆的第一隻羊,接著就可以複製十遍這個程式碼,然後命名不同的羊,以原始sheep為模板進行克隆。
這種方法的弊端:
- 建立新物件,總是需要重新獲取原始物件的屬性值,效率低;
- 總是需要重新初始化物件,而不是動態獲取物件執行時的狀態,不靈活。(什麼意思呢,比如原始的 Sheep 有一項要修改,那麼剩下的以它為範本的,必然要重新初始化)
一、原型模式
- 原型模式指的是,用原型例項指定建立物件的種類,並通過拷貝這些原型,建立新的物件;
- 原型模式是一種建立型設計模式,允許一個物件再建立另一個可以定製的物件,無需知道如何建立的細節;
- 工作原理是:發動建立的這個物件,請求原型物件,讓原型物件來自己實施建立,就是原型物件.clone()。
如下類圖所示:
其中,Prototype 是一個原型介面,在這裡面把克隆自己的方法宣告出來;
ConcreteProtype 可以是一系列的原型類,實現具體操作。
java 的 Object 類是所有類的根類,Object提供了一個 clone() 方法,該方法可以將一個物件複製一份,但是想要實現 clone 的 java 類必須要實現 Cloneable 介面,實現了之後這個類就具有複製的能力。
對於克隆羊問題,我們來利用原型設計模式進行改進:
讓Sheep類,實現 Cloneable 介面:
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 類就是一個具體的原型實現類了,我們想要克隆的時候,客戶端呼叫可以這樣:
Sheep sheep1 = (Sheep) sheep.clone();
Sheep sheep2 = (Sheep) sheep.clone();
//。。。。。類似
這種做法就是原型設計模式。
(spring框架裡,通過bean標籤配置類的scope為prototype,就是用的原型模式)
二、原型模式的淺拷貝、深拷貝問題
使用上面所說的原型模式,按理說是複製出了一模一樣的物件。
但我們做一個嘗試,如果 sheep 類裡的成員變數有一個是物件,而不是基礎型別呢?
private Sheep friend;
然後我們建立、再克隆:
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 後會發現:
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):
- 對於資料型別是基本資料型別的成員變數,淺拷貝會直接進行值傳遞,也就是複製一份給新物件;
- 對於資料型別是引用資料型別的成員變數,淺拷貝會進行引用傳遞,也就是隻是將地址指標複製一份給新物件,實際上覆制前和複製後的內容都指向同一個例項。這種情況,顯然在一個物件裡修改成員變數,會影響到另一個物件的成員變數值(因為修改的都是同一個)
- 預設的 clone() 方法就是淺拷貝。
在原始碼裡也說明了,這個方法是shallow copy 而不是 deep copy。
在實際開發中,往往是希望克隆的過程中,如果類的成員是引用型別,也能完全克隆一份,也就是所謂的深拷貝。
深拷貝(Deep Copy):
- 複製物件的所有基本資料型別成員變數值;
- 為所有 引用資料型別 的成員變數申請儲存空間,並且也複製每個 引用資料型別的成員變數 引用的 所有物件,一直到該物件可達的所有物件;
深拷貝的實現方式,需要通過重寫 clone 方法,或者通過物件的序列化。
下面來實現一下。
2.1 通過重寫 clone 方法深拷貝
/*
被拷貝的類引用的類,此類的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();
}
}
/*
原型類,其中有成員是引用型別,因此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。
//利用序列化實現深拷貝
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方法,把輸入流重新構造成一個物件。
結合上面的程式碼再看看:
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;
三、總結
原型模式:
- 當需要建立一個新的物件的內容比較複雜的時候,可以利用原型模式來簡化建立的過程,同時能夠提高效率。
- 因為這樣不用重新初始化物件,而是動態地獲得物件執行時的狀態,如果原始的物件內部發生變化,其他克隆物件也會發生相應變化,無需一 一修改。
- 實現深拷貝的方法要注意。
缺點:
每一個類都需要一個克隆方法,對於全新的類來說不是問題,但是如果是用已有的類進行改造,那麼可能會因為要修改原始碼而違背 OCP 原則。