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、总结

原型模式的核心在于拷贝原型对象。以系统中已存在的一个对象为原型,直接基于内存二进制流进行拷贝,无需再经历耗时的对象初始化过程(不调用构造函数),性能提高许多。当对象的构建过程比较耗时时,能够利用当前系统中已存在的对象做为原型,对其进行克隆(通常是基于二进制流的复制),躲避初始化过程,使得新对象的建立时间大大减小。
相关文章
相关标签/搜索