1. 程式人生 > >設計模式之原型模式(十三)

設計模式之原型模式(十三)

原型模式是設計模式中算是簡單的一種設計模式了,因為它有語言實現的支撐,只需要呼叫定義好了的方法即可使用,在寫這篇設計模式之前,我看過很多資料,有幾點比較疑惑的點,在這篇文章中驗證,也順便描述一下什麼是原型模式。

 定義:用原型例項指定建立物件的種類,並且通過拷貝這些原型建立新的物件。

這個定義也是很簡單了,主要意思就是用一個例項當作是一個原型,通過拷貝這個例項去建立新的物件,就像西遊記的美猴王使用猴毛去分身,可以很簡單的建立各種美猴王物件,而不需要去初始化各種美猴王屬性,比如身高體重之類。

先上一個簡單的原型模式例項,再丟擲問題。

/**
 * @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(原型模式)的優點

  1. clone方法底層原理是JVM直接複製記憶體塊的操作,所以在速度上比直接new來的快。
  2. 初始化過程比較複雜,類中屬性複雜且需要複製時,使用原型模式更快捷,不需要一個個設定屬性。

 Clone(原型模式)的缺點

從上面的例子也可以看出來,需要去實現一個Cloneable介面以及其clone方法,如果需要克隆的類中有需要深拷貝型別的還需要在clone額外下功夫,比較繁瑣,程式碼量需要增加不少。現實中我們可以在一個類定義好clone方法,需要克隆的類都可以去繼承這個類,也是一個節省程式碼量的做法。

由於這裡的原型模式clone方法並不是平時new物件的操作,所以在克隆的時候不會再執行構造器的方法,而是直接複製記憶體塊,所以要注意的是構造器的方法克隆時不會執行,這裡不做測試,感興趣可以自己在構造器中加入輸出語句方法試一下。