JAVA設計模式之:原型模式

1、定義

原型模式(Prototype-Pattern)是指原型實例指定建立對象的種類,而且經過拷貝這些原型建立新的對象,它屬於建立型模式。

2、應用場景

  咱們先看下下面這個示例:java

public class User {

    private int age;
    private String nickname;
    private String sex;
    private List<String> hobbyList;
    
    ...
}
public class Client {

    public static void main(String[] args) {

        User user1 = new User();
        user1.setAge(22);
        user1.setSex("男");
        user1.setNickname("Theshy");

        User user2 = new User();
        user2.setAge(22);
        user2.setSex("男");
        user2.setNickname("Theshy");

        User user3 = new User();
        user3.setAge(22);
        user3.setSex("男");
        user3.setNickname("Theshy");
    }
}

  在示例中咱們看到有三個用戶,而且三個用戶的信息都是一致的,粗略一看好像沒什麼問題,可是假如咱們有100個一樣信息的用戶,難道咱們就new100個對象出來嗎?這顯然是不可能的。恰巧原型模式就能幫助咱們解決這樣的問題。json

原型模式主要適用於如下場景:

一、類初始化消耗資源較多。
二、new產生的一個對象須要很是繁瑣的過程(數據準備、訪問權限等)
三、構造函數比較複雜。
四、循環體中生產大量相同的對象。

3、原型模式的通用寫法

  一個標準的原型模式代碼,應該是這樣設計的。先建立原型Prototype接口:ide

public interface Prototype<T> {
    T clone();
}
public class User implements Prototype<User>{

    private int age;
    private String nickname;
    private String sex;
    private List<String> hobbyList;

    ...

    @Override
    public User clone() {
        User user = new User();
        user.setAge(this.age);
        user.setNickname(this.nickname);
        user.setSex(this.sex);
        user.setHobbyList(this.hobbyList);
        return user;
    }

    @Override
    public String toString() {
        return "User{" + "age=" + age + ", nickname='" + nickname + '\'' + ", sex='" + sex + '\'' + ", hobbyList=" + hobbyList + '}';
    }
}
public class Client {

    public static void main(String[] args) {
        //建立原型對象
        User user1 = new User();
        user1.setAge(18);
        user1.setNickname("Theshy");
        user1.setSex("男");
        //clone出來的對象
        User user2 = user1.clone();
      
        System.out.println(user1);
        System.out.println(user2);
    }
}

運行結果函數

User{age=18, nickname='Theshy', sex='男', hobbyList=null}
User{age=18, nickname='Theshy', sex='男', hobbyList=null}

  這時候,可能有有會問了,原型模式就這麼簡單嗎?對,就這麼簡單。在這個簡單的場景之下,看上去操做好像變複雜了。但若是有幾百個屬性須要複製,那咱們就能夠一勞永逸。性能

image.png

(該UML圖與示例無關)
從 UML 圖中,咱們能夠看到,原型模式 主要包含三個角色:測試

客戶(Client):客戶類提出建立對象的請求。
抽象原型(Prototype):規定克隆接口。
具體原型(ClonePrototype):被克隆的對象

  雖然上面的複製過程是咱們本身完成的,可是在實際編碼中,咱們通常不會浪費這樣的體力勞動,JDK已經幫咱們實現了一個現成的API,咱們只須要實現Cloneable接口便可。咱們改造一下代碼。ui

package com.ksq.shallow;

import java.util.List;

/**
 * Create By Ke Shuiqiang 2020/3/11 21:34
 */
public class User implements Cloneable{

    private int age;
    private String nickname;
    private String sex;
    private List<String> hobbyList;

    ...

    @Override
    public Object clone(){
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public String toString() {
        return "User{" + "age=" + age + ", nickname='" + nickname + '\'' + ", sex='" + sex + '\'' + ", hobbyList=" + hobbyList + '}';
    }
}

從新運行獲得了一樣的結果this

User{age=18, nickname='Theshy', sex='男', hobbyList=null}
User{age=18, nickname='Theshy', sex='男', hobbyList=null}

  從運行結果看,明明原型對象和克隆對象不是同一個對象,下面咱們再作個測試,修改克隆對象的年齡,暱稱,併爲hobbyList添加元素編碼

public class ShallowCloneTest {

    public static void main(String[] args) {

        //建立原型對象
        User prototype = new User();
        prototype.setAge(18);
        prototype.setNickname("Theshy");
        prototype.setSex("男");

        List<String> hobbyList = new ArrayList<>();
        hobbyList.add("打球");
        hobbyList.add("游泳");
        prototype.setHobbyList(hobbyList);

        //建立克隆對象
        User clonetype = prototype.clone();
        
        //修改克隆對象的年齡,暱稱,並添加一個愛好
        clonetype.setAge(22);
        clonetype.setNickname("zhangsan");
        clonetype.getHobbyList().add("看書");
        System.out.println("原型對象:" + prototype);
        System.out.println("克隆對象:" + clonetype);
        System.out.println("原型對象 == 克隆對象?" + (prototype == clonetype));

    }
}

運行結果spa

原型對象:User{age=18, nickname='Theshy', sex='男', hobbyList=[打球, 游泳, 看書]}
克隆對象:User{age=22, nickname='zhangsan', sex='男', hobbyList=[打球, 游泳, 看書]}
原型對象 == 克隆對象?false

  從運行結果看,原型對象和克隆對象的確不是同一個對象,而且克隆對象的年齡和暱稱也修改爲功了,但是在愛好上好像出了點問題,我明明只爲克隆對象新增了愛好,結果原型對象的愛好也新增,這是怎麼回事呢?

這裏涉及到了原型模式的兩個概念,淺克隆和深克隆:

淺克隆:只負責克隆按值傳遞的數據(好比基本數據類型、String類型),而不克隆它引用的對象,換言之,克隆對象的引用對象都仍然指向原來的引用對象的內存地址。
深克隆:除了克隆按值傳遞的數據,同時也克隆引用類型對象數據,而不是對象的引用指向原來引用對象的內存地址。

  實現深克隆的方式有兩種,一種是經過序列化(二進制流),還有一種是經過JSONObject實現(反射)。

序列化實現深克隆

public User 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 (User) ois.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
public class DeepCloneTest {

    public static void main(String[] args) {

        //建立原型對象
        User prototype = new User();
        prototype.setAge(18);
        prototype.setNickname("Theshy");
        prototype.setSex("男");

        List<String> hobbyList = new ArrayList<>();
        hobbyList.add("打球");
        hobbyList.add("游泳");
        prototype.setHobbyList(hobbyList);

        //建立克隆對象
        User clonetype = prototype.deepClone();

        //修改克隆對象的年齡,暱稱,並添加一個愛好
        clonetype.setAge(22);
        clonetype.setNickname("zhangsan");
        clonetype.getHobbyList().add("看書");
        
        System.out.println("原型對象:" + prototype);
        System.out.println("克隆對象:" + clonetype);
        System.out.println("原型對象 == 克隆對象?" + (prototype == clonetype));

    }
}

運行結果

原型對象:User{age=18, nickname='Theshy', sex='男', hobbyList=[打球, 游泳]}
克隆對象:User{age=22, nickname='zhangsan', sex='男', hobbyList=[打球, 游泳, 看書]}
原型對象 == 克隆對象?false

  從運行結果上能夠看出兩個hobbyList變量是相互獨立的,在修改克隆對象的hobbyList時,原型對象並無改變。這樣就達到了深克隆的目的。

  讓咱們看看深克隆對引用對象有哪些要求。假設如今User對象中添加一個Size屬性;

public class Size{

    private String height;
    private String weight;

    public String getHeight() { return height; }
    public void setHeight(String height) { this.height = height; }
    public String getWeight() { return weight; }
    public void setWeight(String weight) { this.weight = weight; }

    @Override
    public String toString() {
        return "Size{" +
                "height='" + height + '\'' +
                ", weight='" + weight + '\'' +
                '}';
    }
}
public class User implements Cloneable, Serializable{

    private int age;
    private String nickname;
    private String sex;
    private List<String> hobbyList;
    private Size size;
    
    ...
}
public class DeepCloneTest {

    public static void main(String[] args) {

        //建立原型對象
        User prototype = new User();
        prototype.setAge(18);
        prototype.setNickname("Theshy");
        prototype.setSex("男");

        List<String> hobbyList = new ArrayList<>();
        hobbyList.add("打球");
        hobbyList.add("游泳");
        prototype.setHobbyList(hobbyList);
        //添加一個Size對象用於表示用戶的身高和體重
        Size size = new Size();
        size.setHeight("180cm");
        size.setWeight("70Kg");
        prototype.setSize(size);

        //建立克隆對象
        User clonetype = prototype.deepClone();

        //修改克隆對象的年齡,暱稱,並添加一個愛好
        clonetype.setAge(22);
        clonetype.setNickname("zhangsan");
        clonetype.getHobbyList().add("看書");
        //修改克隆對象的體重
        clonetype.getSize().setWeight("80Kg");
        System.out.println("原型對象:" + prototype);
        System.out.println("克隆對象:" + clonetype);
        System.out.println("原型對象 == 克隆對象?" + (prototype == clonetype));

    }
}

運行結果
image.png

  結果顯示報錯,Size對象沒有實現Serializable接口,這說明在想要實現深克隆,從原型對象中的引用對象到該引用對象中的引用對象,都必須實現Serializable,或者引用對象使用transient關鍵字,(這個層次可能會很深,直到你底層對象都實現了Serializable接口,或者是基本數據類型)

使用transient關鍵字

private transient Size size;

運行結果

原型對象:User{age=18, nickname='Theshy', sex='男', hobbyList=[打球, 游泳], size=Size{height='180cm', weight='70Kg'}}
克隆對象:User{age=22, nickname='zhangsan', sex='男', hobbyList=[打球, 游泳, 看書], size=null}
原型對象 == 克隆對象?false

  結果顯示克隆對象的Size對象的爲NULL,很顯然原型對象的Size對象沒有參與到序列化。

實現Serializable接口

public class Size implements Serializable

運行結果

原型對象:User{age=18, nickname='Theshy', sex='男', hobbyList=[打球, 游泳], size=Size{height='180cm', weight='70Kg'}}
克隆對象:User{age=22, nickname='zhangsan', sex='男', hobbyList=[打球, 游泳, 看書], size=Size{height='180cm', weight='80Kg'}}
原型對象 == 克隆對象?false

  能夠從運行結果上看出Size對象也被克隆,而且修改克隆對象weight屬性也成功了。

JSONObject實現深克隆

public class Test {

    public static void main(String[] args) {

        //建立原型對象
        User prototype = new User();
        prototype.setAge(18);
        prototype.setNickname("Theshy");
        prototype.setSex("男");

        List<String> hobbyList = new ArrayList<>();
        hobbyList.add("打球");
        hobbyList.add("游泳");
        prototype.setHobbyList(hobbyList);

        Size size = new Size();
        size.setHeight("180cm");
        size.setWeight("70Kg");
        prototype.setSize(size);

        //import com.alibaba.fastjson.JSONObject;
        //經過JSONObject實現克隆
        User clonetype = JSONObject.parseObject(JSONObject.toJSONString(prototype), User.class);
        //修改克隆對象的年齡,暱稱,並添加一個愛好
        clonetype.setAge(22);
        clonetype.setNickname("zhangsan");
        clonetype.getHobbyList().add("看書");
        clonetype.getSize().setWeight("80Kg");
        System.out.println("原型對象:" + prototype);
        System.out.println("克隆對象:" + clonetype);
        System.out.println("原型對象 == 克隆對象?" + (prototype == clonetype));

    }
}

運行結果

原型對象:User{age=18, nickname='Theshy', sex='男', hobbyList=[打球, 游泳], size=Size{height='180cm', weight='70Kg'}}
克隆對象:User{age=22, nickname='zhangsan', sex='男', hobbyList=[打球, 游泳, 看書], size=Size{height='180cm', weight='80Kg'}}
原型對象 == 克隆對象?false

  運行結果與經過序列化方式的結果是同樣的,而且經過JSONObject實現的深克隆,原型對象中的引用對象不須要Serializable接口,可是由於JSONObject是經過反射實現的,因此在性能上相對沒有序列化方式高。

4、原型模式破壞單例

  既然原型模式是經過二進制流的形式實現克隆的,那麼他是否能破壞單例呢?答案是能夠的。

@Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
public static void main(String[] args) throws Exception {
        LazyStaticInnerClassSingleton instance = LazyStaticInnerClassSingleton.getInstance();
        LazyStaticInnerClassSingleton clone = (LazyStaticInnerClassSingleton)instance.clone();
        System.out.println("instance == clone ? " + (instance == clone));
    }

運行結果

instance == clone ? false

  從結果上看,單例的確被破壞了,其實防止單例被破壞很簡單,禁止克隆即可。要麼咱們的單例類不實現 Cloneable 接口;要麼咱們重寫clone()方法,在clone方法中返回單例對象便可。

@Override 
protected Object clone() throws CloneNotSupportedException{ 
    return INSTANCE;
}

  有一點須要注意的是沒有人會在一個類是單例的狀況下同時又讓這個類是原型模式。由於它們兩個模式本就是相互矛盾的。

5、原型模式的優缺點

優勢

  一、性能優良,Java自帶的 原型模式 是基於內存二進制流的拷貝,比直接new一個對象性能上提高了許多。
  二、可使用深克隆方式保存對象的狀態,使用原型模式將對象複製一份並將其狀態保存起來,簡化了建立對象的過程,以便在須要的時候使用(例如恢復到歷史某一狀態),可輔助實現撤銷操做。

缺點

  一、須要爲每個類配置一個克隆方法。
  二、克隆方法位於類的內部,當對已有類進行改造的時候,須要修改代碼,違反了開閉原則。
  三、在使用序列化實現深克隆時須要編寫較爲複雜的代碼,並且當對象之間存在多重嵌套引用時,爲了實現深克隆,每一層對象對應的類都必須支持深克隆,實現起來會比較麻煩。

6、總結

原型模式的核心在於拷貝原型對象。以系統中已存在的一個對象爲原型,直接基於內存二進制流進行拷貝,無需再經歷耗時的對象初始化過程(不調用構造函數),性能提高許多。當對象的構建過程比較耗時時,能夠利用當前系統中已存在的對象做爲原型,對其進行克隆(通常是基於二進制流的複製),躲避初始化過程,使得新對象的建立時間大大減小。
相關文章
相關標籤/搜索