轻松创建成千上万个对象的原型模式

579 阅读5分钟

介绍

原型模式(prototype Pattern)是指原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象,属于创建型模式。

原型模式的核心在于拷贝原型对象,以系统中已存在的一个对象为原型,无需经历耗时的对象初始化过程,不调用 new 构造函数。

我们看下UML图:

image.png

通用写法

看完上面的介绍,我们来写一个原型模式的通用写法。先创建原型IPrototype接口:

public interface IPrototype<T> {
    T clone();
}

创建具体需要克隆的对象ConcretePrototype

@Data
public class ConcretePrototype implements IPrototype {

    private int age;

    private String name;


    @Override
    public ConcretePrototype clone() {
        ConcretePrototype concretePrototype = new ConcretePrototype();
        concretePrototype.setAge(this.age);
        concretePrototype.setName(this.name);
        return concretePrototype;
    }

}

测试代码:

    public static void main(String[] args) {
        //创建原型对象
        ConcretePrototype prototype = new ConcretePrototype();
        prototype.setAge(18);
        prototype.setName("Jack");
        System.out.println(prototype);

        //拷贝原型对象
        ConcretePrototype cloneType = prototype.clone();
        System.out.println(cloneType);
    }

运行结果:

image.png

一个简单的原型模式就写完了,在这个简单的场景之下,看上去操作好像变的复杂了,但如果有几百个属性需要复制,那我们就可以一劳永逸。但是,上面的复制过程是我们自己完成的,在实际编码中,我们一般不会浪费这样的体力劳动,JKD已经帮我们实现了一个现成的API,我们只需要实现Cloneable接口即可。

浅克隆

新建一个ShallowConcretePrototype类实现Cloneable接口

@Data
public class ShallowConcretePrototype implements Cloneable {

    private int age;

    private String name;

    private List<String> hobbies;

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

}

写一段测试代码

    public static void main(String[] args) {
        //创建原型对象
        ShallowConcretePrototype prototype = new ShallowConcretePrototype();
        prototype.setAge(18);
        prototype.setName("Jack");
        List<String> hobbies = new ArrayList<String>();
        hobbies.add("看书");
        hobbies.add("旅游");
        prototype.setHobbies(hobbies);

        //拷贝原型对象
        ShallowConcretePrototype cloneType = prototype.clone();
        System.out.println("原型对象:" + prototype);
        System.out.println("克隆对象:" + cloneType);

        System.out.println("——————————————————————分割线————————————————————————");
        cloneType.getHobbies().add("技术控");
        System.out.println("原型对象:" + prototype);
        System.out.println("克隆对象:" + cloneType);
        System.out.println(prototype.getHobbies() == cloneType.getHobbies());
    }

执行结果:

image.png

我们来解读一下,首先创建一个原型对象,然后使用clone方法创建一个克隆对象,打印一下,两者是一样的,没问题。然后我给克隆对象的爱好添加了一个技术控,然后在打印一下,发现原型对象的爱好里也多了一个技术控,这个是不符合我们预期的,我们希望的是克隆对象和原型对象是两个独立的对象,我添加我的,你添加你的,不能因为我添加了影响到你。

上面的这个现象就叫浅克隆,通过我们分析下来,应该是hobbies共用了一个内存地址,意味着复制的不是值,而是引用的地址。最后我们通过prototype.getHobbies() == cloneType.getHobbies()测试为true,证明确实它们的地址是一样的。

深克隆

为了解决上面的问题,下面就引出我们的深克隆,创建一个DeepConcretePrototype类,增加一个deepClone的方法。

@Data
public class DeepConcretePrototype implements Cloneable, Serializable {

    private int age;
    private String name;
    private List<String> hobbies;

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

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

}

测试代码和上面一样,只不过在克隆的时候使用的是deepClone方法,而不是clone方法

    public static void main(String[] args) {
        //创建原型对象
        DeepConcretePrototype prototype = new DeepConcretePrototype();
        prototype.setAge(18);
        prototype.setName("Jack");
        List<String> hobbies = new ArrayList<String>();
        hobbies.add("看书");
        hobbies.add("旅游");
        prototype.setHobbies(hobbies);

        //拷贝原型对象
        DeepConcretePrototype cloneType = prototype.deepClone();
        System.out.println("原型对象:" + prototype);
        System.out.println("克隆对象:" + cloneType);

        System.out.println("——————————————————————分割线————————————————————————");
        cloneType.getHobbies().add("技术控");
        System.out.println("原型对象:" + prototype);
        System.out.println("克隆对象:" + cloneType);
        System.out.println(prototype.getHobbies() == cloneType.getHobbies());
    }

执行一下,这时候就达到了我们预期想要的效果了。 image.png

在这里deepClone方法中用的是输入输出流的方法,还可以用Jackson的序列化和反序列话来实现深克隆;也可以不增加deepClone方法,直接在clone方法里调用super.clone(),然后手动的给hobbies赋值。

原型保证单例

如果有一个类实现了Cloneable接口,又要保证它单例,那么只需要在clone方法中返回单例对象即可。

@Data
public class SingletonConcretePrototype implements Cloneable {

    private int age;
    private String name;
    private List<String> hobbies;

    private static SingletonConcretePrototype instance = new SingletonConcretePrototype();

    private SingletonConcretePrototype() {}

    public static SingletonConcretePrototype getInstance() {
        return instance;
    }

    @Override
    public SingletonConcretePrototype clone() {
        return instance;
    }
}

或者就是不实现Cloneable接口,因为本身单例和原型是矛盾的,单例要求的是全局只有一个对象,而原型则是创造出很多个对象,那到底是一个还是多个?这一点在Sping中的scope就是要么填singleton要么填prototype。

更多关于单例的知识请点击:《深入解析单例模式的写法以及破坏单例方式》

原型模式应用

在Java代码中查找实现了Cloneable接口的类,我们以ArrayList为例:

    public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }

这里将List中的元素循环遍历了一遍,这个就是深克隆,我们来验证一下

    public static void main(String[] args) {
        ArrayList<String> stringList = new ArrayList<>();
        stringList.add("a");
        stringList.add("b");
        stringList.add("c");

        ArrayList<String> cloneList = (ArrayList<String>) stringList.clone();
        System.out.println("原型对象:" + stringList);
        System.out.println("克隆对象:" + cloneList);
        System.out.println("——————————————————————分割线————————————————————————");
        cloneList.add("d");
        System.out.println("原型对象:" + stringList);
        System.out.println("克隆对象:" + cloneList);
    }

添加完d后,没有影响到原型对象,证明是深克隆。 image.png

优缺点

优点:

  1. 性能优良,Java自带的原型模式是基于内存二进制流的拷贝,比直接new一个对象性能上提升了许多。
  2. 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。

缺点:

  1. 需要为每一个类配置一个克隆方法。
  2. 克隆方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违反了开闭原则。
  3. 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦。

深克隆、浅克隆看具体应用场景选择。 本文源码在:github.com/xuhaoj/patt…