原型模式

一、概述

一般问题:有时系统中需要创建重复对象,而这些对象的构造函数比较复杂耗时。

核心方案:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

设计意图:每当说到创建一个对象实例,我们总是想到调用构造函数new一个实例;实际上除了凭空创造一个新实例,还可以通过已有实例克隆一个实例。克隆比new的效率更高,尤其是当构造函数复杂耗时,比如需要读取数据库。

原型模式类图:


 

二、应用实践

(1)浅克隆与深克隆

在Java中要想克隆一个对象,必须实现空接口Cloneable,如:

    /**
     * Cloneable是一个空接口(标记接口),是一个规范。但是如果要克隆这个类对象的话必须实现Cloneable接口
     */
    public static class ProtoTypeTest implements Cloneable{
        private int testInt = 0;
        private String testString = "hello";
        private TestObj mObj = new TestObj();
        public ProtoTypeTest(){}

        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone(); //这里不需要我们主动为每一个变量赋值,clone过程有底层方法直接复制内存实现
        }
    }

    public static class TestObj{
        public int objInt = 2;
        public TestObj(){}
    }

其中mObj是引用数据类型属性,testInt和testString是基本数据类型属性,它们在克隆时的表现是有区别的。

我们写一个测试demo:

    ProtoTypeTest proto1 = new ProtoTypeTest(); //创建一个ProtoTypeTest实例proto1
       try {
            ProtoTypeTest proto2 = (ProtoTypeTest) proto1.clone(); //根据proto1克隆一个ProtoTypeTest实例proto2
            proto2.testInt = -1; //改变proto2的三个属性值并对比proto1和proto2的属性值
            proto2.testString = "hi";
            proto2.mObj.objInt = -2;
            Log.i("proto","proto1=(" + proto1.testInt + "," + proto1.testString + "," + proto1.mObj.objInt + ")");
            Log.i("proto","proto2=(" + proto2.testInt + "," + proto2.testString + "," + proto2.mObj.objInt + ")");
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }

输出结果:

06-19 13:37:29.128 14682 14682 I proto   : proto1=(0,hello,-2)
06-19 13:37:29.128 14682 14682 I proto   : proto2=(-1,hi,-2)

我们发现修改proto2的前两个基本类型属性时,proto1的对应属性并没有一起被修改;但是修改proto2的第三个引用类型属性时,proto1对应的属性也发生了变化。

原因是java的clone函数只是“浅克隆”,也就是仅对变量内存做复制,如果是引用变量,其内存指向是不变的。

如果要做“深克隆”,即内存指向也克隆一份,需要在clone函数中单独对引用变量克隆,如下:

    /**
     * Cloneable是一个空接口(标记接口),是一个规范。但是如果要克隆这个类对象的话必须实现Cloneable接口
     */
    public static class ProtoTypeTest implements Cloneable{
        private int testInt = 0;
        private String testString = "hello";
        private TestObj mObj = new TestObj();
        public ProtoTypeTest(){}

        @Override
        protected Object clone() throws CloneNotSupportedException {
            ProtoTypeTest cloneProto = (ProtoTypeTest) super.clone(); //这里不需要我们主动为每一个变量赋值,clone过程有底层方法直接复制内存实现
            cloneProto.mObj = (TestObj) mObj.clone(); //这里对TestObj引用变量单独克隆,同时TestObj也需要实现Cloneable接口
            return cloneProto;
        }
    }

    public static class TestObj implements Cloneable{
        public int objInt = 2;
        public TestObj(){}

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

输出结果:

06-19 14:21:58.506 11297 11297 I proto   : proto1=(0,hello,2)
06-19 14:21:58.506 11297 11297 I proto   : proto2=(-1,hi,-2)

这样proto2和proto1就完全独立分开,互不影响了。

(2)通过序列化与反序列化实现深克隆

除了上面的通过为每个引用变量单独克隆实现“深克隆”,还可以通过序列化和反序列化实现“深克隆”。

首先,克隆对象和内部引用对象都需要实现序列化接口

    /**
     * 实现序列化接口
     */
    public static class ProtoTypeTest implements Serializable{
        private int testInt = 0;
        private String testString = "hello";
        private TestObj mObj = new TestObj();
        public ProtoTypeTest(){}
    }

    //实现序列化接口
    public static class TestObj implements Serializable{
        public int objInt = 2;
        public TestObj(){}
    }

通过序列化实现“深克隆”

         ProtoTypeTest proto1 = new ProtoTypeTest(); //创建一个ProtoTypeTest实例proto1
                try {
            //ProtoTypeTest proto2 = (ProtoTypeTest) proto1.clone(); //根据proto1克隆一个ProtoTypeTest实例proto2

                    //下面使用序列化和反序列化实现"深克隆"
                    //1、将s1对象序列化为一个数组
                    ByteArrayOutputStream bos = new ByteArrayOutputStream(); //通过ObjectOutputStream流将s1对象读出来给ByteArrayOutputStream流
                    ObjectOutputStream    oos = new ObjectOutputStream(bos);
                    oos.writeObject(proto1);
                    byte[] bytes = bos.toByteArray(); //ByteArrayOutputStream流将对象信息转成byte数组,这样byte数组里就包含了对象的数据

                    //2、将字节数组中的内容反序列化为一个Sheep对象
                    ByteArrayInputStream bis = new ByteArrayInputStream(bytes); //通过ByteArrayInputStream流读入bytes字节数组中数据,然后传给ObjectInputStream对象输入流
                    ObjectInputStream ois = new ObjectInputStream(bis);
                    ProtoTypeTest proto2 = (ProtoTypeTest) ois.readObject(); //通过ObjectInputStream返回一个Sheep对象

                    proto2.testInt = -1; //改变proto2的三个属性值并对比proto1和proto2的属性值
                    proto2.testString = "hi";
                    proto2.mObj.objInt = -2;
                    Log.i("proto","proto1=(" + proto1.testInt + "," + proto1.testString + "," + proto1.mObj.objInt + ")");
                    Log.i("proto","proto2=(" + proto2.testInt + "," + proto2.testString + "," + proto2.mObj.objInt + ")");
                } catch (Exception e) {
                    e.printStackTrace();
                }

输出结果和(1)中的“深克隆”结果一样:

06-19 14:54:24.503 28741 28741 I proto   : proto1=(0,hello,2)
06-19 14:54:24.503 28741 28741 I proto   : proto2=(-1,hi,-2)

通过序列化和反序列化实现“深克隆”,不再需要实现cloneable接口,也不再需要对每个引用变量单独clone了。

(3)Android中的原型模式

Android中也有很多用到原型模式的地方,如Intent

public class Intent implements Parcelable, Cloneable {

    @Override
    public Object clone() {
        return new Intent(this);
    }
   /**
     * Copy constructor.
     */
    public Intent(Intent o) {
        this(o, COPY_MODE_ALL);
    }
}

通过查看源码,Intent的clone实际上还是调用了new构造函数并为各个属性重新赋值。尽管在赋值时对个别属性做了条件判断,但个人感觉这样写并没有多大性能提升,反而可能仅仅是为了让Intent支持clone而已。


 

三、总结

总结:原型模式是一种创建型设计模式,当类初始化需要消化非常多的资源,这个资源包括数据、硬件资源、权限访问等时,可以考虑使用原型模式。除了优化性能之外,原型模式还可以对原实例起到保护作用。

用一句话表述原型模式:

如果生一个太麻烦,那就克隆吧~

优点:

  • 性能提高
  • 避免构造函数的约束

缺点:

  • 如类已经存在,则需要改动原类代码
  • 必须实现Cloneable或序列化接口

 

posted @ 2019-06-19 13:46  西贝雪  阅读(604)  评论(0编辑  收藏  举报