設計模式之原型模式(十三)
原型模式是設計模式中算是簡單的一種設計模式了,因為它有語言實現的支撐,只需要呼叫定義好了的方法即可使用,在寫這篇設計模式之前,我看過很多資料,有幾點比較疑惑的點,在這篇文章中驗證,也順便描述一下什麼是原型模式。
定義:用原型例項指定建立物件的種類,並且通過拷貝這些原型建立新的物件。
這個定義也是很簡單了,主要意思就是用一個例項當作是一個原型,通過拷貝這個例項去建立新的物件,就像西遊記的美猴王使用猴毛去分身,可以很簡單的建立各種美猴王物件,而不需要去初始化各種美猴王屬性,比如身高體重之類。
先上一個簡單的原型模式例項,再丟擲問題。
/** * @description: * @author: linyh * @create: 2018-11-05 16:27 **/ public class Weapon { private int size; public Weapon(int size) { this.size = size; } public int getSize() { return size; } public void changeSize(){ size++; } }
/** * @description: * @author: linyh * @create: 2018-11-05 16:27 **/ public class Monkey implements Cloneable{ private int age; private String name; private Weapon weapon; public Monkey() { this.age = 18; this.name = "猴子"; this.weapon = new Weapon(10); } public void changeAge(){ age ++; } public void changeName(){ name = name + "changed "; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } public int getAge() { return age; } public String getName() { return name; } public Weapon getWeapon() { return weapon; } }
monkey中的change方法後面實驗會用到。下面上測試類。
public class test { public static void main(String[] args) throws CloneNotSupportedException { Monkey monkey = new Monkey(); Monkey copyMonkey = (Monkey)monkey.clone(); System.out.println("兩個物件是否一樣: " + (monkey == copyMonkey)); System.out.println("正版猴子的各個屬性: " + monkey.getName() +"," + monkey.getAge()); System.out.println("複製猴子的各個屬性: " + copyMonkey.getName() +"," + copyMonkey.getAge()); System.out.println("兩個猴子的武器是否一樣: " + (monkey.getWeapon() == copyMonkey.getWeapon())); } }
控制檯列印
這裡發現,克隆出來的物件是不一樣的,如預期所料,屬性都有初始化好,然後物件不同。但是這裡的武器卻還是同一個武器!這會引發什麼問題呢?上測試程式碼
System.out.println("正版的猴子的武器SIZE: " + monkey.getWeapon().getSize());
System.out.println("複製的猴子的武器SIZE: " + copyMonkey.getWeapon().getSize());
monkey.getWeapon().changeSize();
System.out.println("正版猴子武器SIZE改變成了" +monkey.getWeapon().getSize());
System.out.println("複製的猴子的武器SIZE: " + copyMonkey.getWeapon().getSize());
控制檯列印
可以看到,因為是同一個物件,我正版猴子把武器的size變化了,複製的猴子什麼都沒幹武器也會變化,這確實不符合邏輯。
以上覆制稱為淺複製,意思是如果有引用型別,會把引用的地址直接複製過來,但如果是8種基本型別就沒事,下面驗證一下。
System.out.println("正版的猴子的年齡: " + monkey.getAge());
System.out.println("複製的猴子的年齡: " + copyMonkey.getAge());
monkey.changeAge();
System.out.println("正版猴子年齡改變成了" + monkey.getAge());
System.out.println("複製的猴子的年齡: " + copyMonkey.getAge());
控制檯列印
雖然我這裡改變了正版猴子的年齡,但複製的猴子年齡依舊的18。
丟擲第一個問題:哪些型別需要深拷貝?
我曾經在一個知名博主的一篇部落格上看到一個論點,表示很疑惑。
因為我不記得在哪篇文章上有看到說包括9種類型(8種基本型別加上String型別),都不需要深拷貝 ,驗證一下就知道了。
System.out.println("正版的猴子的姓名: " + monkey.getName());
System.out.println("複製的猴子的姓名: " + copyMonkey.getName());
monkey.changeName();
System.out.println("正版猴子姓名變成了" + monkey.getName());
System.out.println("複製的猴子的姓名: " + copyMonkey.getName());
控制檯列印
查了一下資料,發現String 不改變值是淺copy。但給name賦值時(set方法等),常量值的地址是固定不變的,字串物件只能改變自己的引用了,原來物件的引用不會變,效果上這是相當於實現了深copy。
所以這裡真正的結論是:8種基本型別加上String型別,都可以不需要深拷貝,底層自動會進行深拷貝。
丟擲第二個問題:實現深拷貝的方法?
據我所知有兩種,分別測試一下把。
第一種方法:將需要深拷貝的引用物件再次呼叫clone方法
@Override
protected Object clone() throws CloneNotSupportedException {
Monkey monkey =(Monkey) super.clone();
monkey.weapon = (Weapon) this.weapon.clone();
return monkey;
}
這裡將Monkey的clone方法改造了一下,所以Weapon中也需要加入clone方法。
/**
* @description:
* @author: linyh
* @create: 2018-11-05 16:27
**/
public class Weapon implements Cloneable{
private int size;
public Weapon(int size) {
this.size = size;
}
public int getSize() {
return size;
}
public void changeSize(){
size++;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
測試一下
這裡的結果就符合了我們預期的效果了。
第二種方法:序列化再反序列化
改變一下Monkey的clone方法,此方法需要將需要深拷貝的類實現Serializable介面(Monkey、Weapon)
@Override
protected Object clone() throws CloneNotSupportedException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
try {
//序列化
oos = new ObjectOutputStream(bos);
oos.writeObject(this);
//反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);
return ois.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}finally {
try {
oos.close();
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
這裡給Monkey多加一個屬性以及get和change方法便於測試。
private List<Integer> list;
public void changeList(){ list.add(2); }
測試類
Monkey monkey = new Monkey();
Monkey copyMonkey = (Monkey)monkey.clone();
System.out.println("兩個物件是否一樣: " + (monkey == copyMonkey));
System.out.println("正版猴子的各個屬性: " + monkey.getName() +"," + monkey.getAge());
System.out.println("複製猴子的各個屬性: " + copyMonkey.getName() +"," + copyMonkey.getAge());
System.out.println("兩個猴子的武器是否一樣: " + (monkey.getWeapon() == copyMonkey.getWeapon()));
System.out.println("兩個猴子的List是否一樣: " + (monkey.getList() == copyMonkey.getList()));
System.out.println("正版的猴子的武器SIZE: " + monkey.getWeapon().getSize());
System.out.println("複製的猴子的武器SIZE: " + copyMonkey.getWeapon().getSize());
monkey.getWeapon().changeSize();
System.out.println("正版猴子武器SIZE改變成了" +monkey.getWeapon().getSize());
System.out.println("複製的猴子的武器SIZE: " + copyMonkey.getWeapon().getSize());
System.out.println("正版的猴子的ListSize: " + monkey.getList().size());
System.out.println("複製的猴子的ListSize: " + copyMonkey.getList().size());
monkey.changeList();
System.out.println("正版猴子ListSize改變成了" +monkey.getList().size());
System.out.println("複製的猴子的ListSize: " + copyMonkey.getList().size());
控制檯列印
這裡就完成了深拷貝過程,對比兩種方法,後者,只需要序列化與反序列化以及實現序列化介面即可,如果是第一種方法,需要將所有的需要深拷貝的引用都呼叫其上的clone方法,對比而言,像上面一個例子既有list還有weapon這種多個需要深拷貝引用的,個人覺得還是使用第二種序列化的方式比較便捷,如果只有一個引用像第一種的例子只有一個weapon需要深拷貝,就可以考慮weapon也實現一個clone,然後在monkey呼叫clone的時候也把weapon clone一下,畢竟只有一個。
Clone(原型模式)的優點
- clone方法底層原理是JVM直接複製記憶體塊的操作,所以在速度上比直接new來的快。
- 初始化過程比較複雜,類中屬性複雜且需要複製時,使用原型模式更快捷,不需要一個個設定屬性。
Clone(原型模式)的缺點
從上面的例子也可以看出來,需要去實現一個Cloneable介面以及其clone方法,如果需要克隆的類中有需要深拷貝型別的還需要在clone額外下功夫,比較繁瑣,程式碼量需要增加不少。現實中我們可以在一個類定義好clone方法,需要克隆的類都可以去繼承這個類,也是一個節省程式碼量的做法。
由於這裡的原型模式clone方法並不是平時new物件的操作,所以在克隆的時候不會再執行構造器的方法,而是直接複製記憶體塊,所以要注意的是構造器的方法克隆時不會執行,這裡不做測試,感興趣可以自己在構造器中加入輸出語句方法試一下。