本文節選自《設計模式就該這樣學》
1 分析JDK淺克隆API帶來的問題
在Java提供的API中,不需要手動創建抽象原型接口,因為Java已經內置了Cloneable抽象原型接口,自定義的類型只需實現該接口並重寫Object.clone()方法即可完成本類的復制。
通過查看JDK的源碼可以發現,其實Cloneable是一個空接口。Java之所以提供Cloneable接口,只是為了在運行時通知Java虛擬機可以安全地在該類上使用clone()方法。而如果該類沒有實現 Cloneable接口,則調用clone()方法會拋出 CloneNotSupportedException異常。
一般情況下,如果使用clone()方法,則需滿足以下條件。
(1)對任何對象o,都有o.clone() != o。換言之,克隆對象與原型對象不是同一個對象。
(2)對任何對象o,都有o.clone().getClass() == o.getClass()。換言之,復制對象與原對象的類型一樣。
(3)如果對象o的equals()方法定義恰當,則o.clone().equals(o)應當成立。
我們在設計自定義類的clone()方法時,應當遵守這3個條件。一般來說,這3個條件中的前2個是必需的,第3個是可選的。
下面使用Java提供的API應用來實現原型模式,代碼如下。
class Client {
public static void main(String[] args) {
//創建原型對象
ConcretePrototype type = new ConcretePrototype("original");
System.out.println(type);
//復制原型對象
ConcretePrototype cloneType = type.clone();
cloneType.desc = "clone";
System.out.println(cloneType);
}
static class ConcretePrototype implements Cloneable {
private String desc;
public ConcretePrototype(String desc) {
this.desc = desc;
}
@Override
protected ConcretePrototype clone() {
ConcretePrototype cloneType = null;
try {
cloneType = (ConcretePrototype) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return cloneType;
}
@Override
public String toString() {
return "ConcretePrototype{" +
"desc='" + desc + '\'' +
'}';
}
}
}
super.clone()方法直接從堆內存中以二進制流的方式進行復制,重新分配一個內存塊,因此其效率很高。由於super.clone()方法基於內存復制,因此不會調用對象的構造函數,也就是不需要經歷初始化過程。
在日常開發中,使用super.clone()方法並不能滿足所有需求。如果類中存在引用對象屬性,則原型對象與克隆對象的該屬性會指向同一對象的引用。
@Data
public class ConcretePrototype implements Cloneable {
private int age;
private String name;
private List<String> hobbies;
@Override
public ConcretePrototype clone() {
try {
return (ConcretePrototype)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
@Override
public String toString() {
return "ConcretePrototype{" +
"age=" + age +
", name='" + name + '\'' +
", hobbies=" + hobbies +
'}';
}
}
修改客戶端測試代碼。
public static void main(String[] args) {
//創建原型對象
ConcretePrototype prototype = new ConcretePrototype();
prototype.setAge(18);
prototype.setName("Tom");
List<String> hobbies = new ArrayList<String>();
hobbies.add("書法");
hobbies.add("美術");
prototype.setHobbies(hobbies);
System.out.println(prototype);
//復制原型對象
ConcretePrototype cloneType = prototype.clone();
cloneType.getHobbies().add("技術控");
System.out.println("原型對象:" + prototype);
System.out.println("克隆對象:" + cloneType);
}
我們給復制的對象新增一個屬性hobbies(愛好)之后,發現原型對象也發生了變化,這顯然不符合預期。因為我們希望復制出來的對象應該和原型對象是兩個獨立的對象,不再有聯系。從測試結果來看,應該是hobbies共用了一個內存地址,意味着復制的不是值,而是引用的地址。這樣的話,如果我們修改任意一個對象中的屬性值,protoType 和cloneType的hobbies值都會改變。這就是我們常說的淺克隆。只是完整復制了值類型數據,沒有賦值引用對象。換言之,所有的引用對象仍然指向原來的對象,顯然不是我們想要的結果。那如何解決這個問題呢?
Java自帶的clone()方法進行的就是淺克隆。而如果我們想進行深克隆,可以直接在super.clone()后,手動給復制對象的相關屬性分配另一塊內存,不過如果當原型對象維護很多引用屬性的時候,手動分配會比較煩瑣。因此,在Java中,如果想完成原型對象的深克隆,則通常使用序列化(Serializable)的方式。
2 使用序列化實現深克隆
在上節的基礎上繼續改造,增加一個deepClone()方法。
/**
* Created by Tom.
*/
@Data
public class ConcretePrototype implements Cloneable,Serializable {
private int age;
private String name;
private List<String> hobbies;
@Override
public ConcretePrototype clone() {
try {
return (ConcretePrototype)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
public ConcretePrototype deepClone(){
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (ConcretePrototype)ois.readObject();
}catch (Exception e){
e.printStackTrace();
return null;
}
}
@Override
public String toString() {
return "ConcretePrototype{" +
"age=" + age +
", name='" + name + '\'' +
", hobbies=" + hobbies +
'}';
}
}
客戶端調用代碼如下。
public static void main(String[] args) {
//創建原型對象
ConcretePrototype prototype = new ConcretePrototype();
prototype.setAge(18);
prototype.setName("Tom");
List<String> hobbies = new ArrayList<String>();
hobbies.add("書法");
hobbies.add("美術");
prototype.setHobbies(hobbies);
//復制原型對象
ConcretePrototype cloneType = prototype.deepCloneHobbies();
cloneType.getHobbies().add("技術控");
System.out.println("原型對象:" + prototype);
System.out.println("克隆對象:" + cloneType);
System.out.println(prototype == cloneType);
System.out.println("原型對象的愛好:" + prototype.getHobbies());
System.out.println("克隆對象的愛好:" + cloneType.getHobbies());
System.out.println(prototype.getHobbies() == cloneType.getHobbies());
}
運行程序,得到如下圖所示的結果,與期望的結果一致。
從運行結果來看,我們的確完成了深克隆。
關注微信公眾號『 Tom彈架構 』回復“設計模式”可獲取完整源碼。
本文為“Tom彈架構”原創,轉載請注明出處。技術在於分享,我分享我快樂!
如果本文對您有幫助,歡迎關注和點贊;如果您有任何建議也可留言評論或私信,您的支持是我堅持創作的動力。關注微信公眾號『 Tom彈架構 』可獲取更多技術干貨!