大家在日常開發的過程中是否遇到過如下的煩惱:
- 建立一個對象太複雜了,而且建立的對象大多數屬性都是一緻的,隻有個别字段值不同
- 資料庫讀到資料後,在service層處理的時候需要不停的set/get,顯得很費力
- 業務擴充,某個實體需要新增字段,需要在其他涉及該實體指派的對象需要補充set/get
上面的問題都太常見,又太痛苦了,那麼怎麼解決這個問題呢?我們的原型設計模式閃亮登場!
**概念:**原型模式(Prototype Pattern)是指用原型執行個體指定建立對象的種類,并且通過拷貝這些原型建立新的對象。
顧名思義就是根據已有對象,複制一個新對象。
一、淺克隆
典型的原型模式需要我們自己建立一個Prototype接口,并定義一個clone接口,将需要複制的對象的類實作該接口,并重寫clone方法,在clone方法中實作具體的細節。但是java給我們提供了一個非常好的解決方法,這裡隻介紹java給我們提供的clone方式。
先建立一個類,叫MingRen,它有性别、年齡、技能清單、寵物四個屬性:
public class MingRen implements Cloneable {
private String sex;
private int age;
private List<String> skills;
private Pet pet;
//為節省篇幅set/get方法略
...
@Override
public String toString() {
return "MingRen{" +
"sex='" + sex + '\'' +
", age=" + age +
", skills=" + skills +
", pet=" + pet +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
其中寵物類如下:
public class Pet {
private String name;
private String type;
private List<String> skills;
...
@Override
public String toString() {
return "Pet{" +
"name='" + name + '\'' +
", type='" + type + '\'' +
", skills=" + skills +
'}';
}
}
然後我們在測試類中先初始化一個鳴人:
public class PrototypeTest {
public static void main(String[] args) {
MingRen mingRen = new MingRen();
mingRen.setSex("male");
mingRen.setAge(15);
List<String> mingrenSkills = new ArrayList<>();
mingrenSkills.add("螺旋丸");
mingrenSkills.add("影分身");
mingRen.setSkills(mingrenSkills);
Pet hamaji = new Pet();
hamaji.setName("蛤蟆吉");
hamaji.setType("蛤蟆");
List<String> hamajiSkills = new ArrayList<>();
hamajiSkills.add("找人");
hamajiSkills.add("搞笑");
hamaji.setSkills(hamajiSkills);
mingRen.setPet(hamaji);
System.out.println("沒克隆前的源對象:" + mingRen);
}
}
列印的結果如下:
沒克隆前的源對象:MingRen{sex='male', age=15, skills=[螺旋丸, 影分身], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
可以看到,這個對象的建立已經有一定的複雜性了,既要考慮鳴人自己的技能,還得考慮他召喚出來的召喚獸的技能。而一旦鳴人釋放影分身之術,将會有大量的鳴人1、2、3出現,每一個都是真的,總不能每個都這麼建立一個吧。于是呼,隻需要讓鳴人類實作Cloneable接口,并重寫clone方法,調用并傳回其父類的clone方法,就可以調用clone方法複制這個對象了。
測試代碼新增clone方法如下:
try{
MingRen mingRen1 = (MingRen) mingRen.clone();
mingRen1.setSex("female");
mingRen1.getSkills().add("手裡劍");
System.out.println("克隆後的源對象:" + mingRen);
System.out.println("克隆對象:" + mingRen1);
System.out.println(mingRen.getSkills() == mingRen1.getSkills());
System.out.println(mingRen.getPet() == mingRen1.getPet());
}catch (CloneNotSupportedException e){
e.printStackTrace();
}
看看輸出結果:
沒克隆前的源對象:MingRen{sex='male', age=15, skills=[螺旋丸, 影分身], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
克隆後的源對象:MingRen{sex='male', age=15, skills=[螺旋丸, 影分身, 手裡劍], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
克隆對象:MingRen{sex='female', age=15, skills=[螺旋丸, 影分身, 手裡劍], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
true
true
先看看克隆對象,鳴人釋放色誘之術影分身可以看成是女的。隻修改了sex屬性,并且該分身為了保護自己新增了一個手裡劍技能,結果看起來很完美,所有屬性一個不落的複制到了。
接下來再看看對克隆後的對象進行修改,會發現源對象也被修改了。說明這個克隆對象的方法是存在一定局限性的。隻有基本類型和String類型可以真複制,其他的引用類型複制的都是其引用位址。如果需要複制對象屬性而不是引用則需要用到深克隆的技術。
在将深克隆之前,先介紹一個spring為我們提供的BeanUtils裡的copyProperties方法,api很簡單隻需要傳入source對象及target對象即可。樣例代碼如下:
MingRen mingRen3 = new MingRen();
BeanUtils.copyProperties(mingRen, mingRen3);
但是這樣複制出來的仍然是淺克隆,用來作為指派非常友善,還可以選擇忽略哪些字段不複制,更加詳細的将會在spring源碼解析中講解。
二、深克隆
1、利用序列化來實作深克隆,将MingRen及Pet實作Serializable接口,并補充如下代碼:
@Override
protected Object clone(){
return deepClone();
}
private Object 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);
MingRen mingRenCopy = (MingRen) ois.readObject();
ois.close();
bis.close();
oos.close();
bos.close();
return mingRenCopy;
}catch (Exception e){
e.printStackTrace();
return null;
}
}
再次運作測試類,将會得到如下結果:
沒克隆前的源對象:MingRen{sex='male', age=15, skills=[螺旋丸, 影分身], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
克隆後的源對象:MingRen{sex='male', age=15, skills=[螺旋丸, 影分身], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
克隆對象:MingRen{sex='female', age=15, skills=[螺旋丸, 影分身, 手裡劍], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
false
false
很顯然這樣的克隆確定了每一個引用對象都是新的。細心的人應該會問,為什麼都是引用類型,我隻給Pet實作序列化呢?檢視源碼就知道了,ArrayList本身已經實作了序列化,是以利用序列化及反序列化是完全可行的。
2、利用fastjson複制對象
這個也是項目中經常用到的,隻需要引入fastjson的包,直接調用其api即可,代碼如下:
//利用FastJson複制對象
String json = JSON.toJSONString(mingRen);
MingRen mingRen2 = JSON.parseObject(json, MingRen.class);
mingRen2.setSex("female");
mingRen2.getSkills().add("手裡劍");
System.out.println("克隆後的源對象:" + mingRen);
System.out.println("克隆對象:" + mingRen2);
System.out.println(mingRen.getSkills() == mingRen2.getSkills());
System.out.println(mingRen.getPet() == mingRen2.getPet());
輸出結果如下:
沒克隆前的源對象:MingRen{sex='male', age=15, skills=[螺旋丸, 影分身], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
克隆後的源對象:MingRen{sex='male', age=15, skills=[螺旋丸, 影分身], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
克隆對象:MingRen{sex='female', age=15, skills=[螺旋丸, 影分身, 手裡劍], pet=Pet{name='蛤蟆吉', type='蛤蟆', skills=[找人, 搞笑]}}
false
false
可以看出,複制出來的對象也是完全ok的
三、原型模式與單例模式
可以看出,原型模式和單例模式是完全相悖的。那麼如果要保持單例模式,那麼還需要在單例模式裡加上一些限制。
1、不實作Cloneable接口
不實作該接口,也就不用重寫clone方法,自然就不會存在克隆。
2、重寫clone方法
如果已經實作了,那麼久重寫clone方法,讓其直接放回單例的執行個體,或者直接傳回getInstance方法也是非常友善的
小結:原型模式是非常簡單的一種設計模式,但是在實際項目開發中确實使用率相當高的一種模式。或許你已經在使用了,隻是還沒有意識到罷了!