原型模式
一、概述
一般问题:有时系统中需要创建重复对象,而这些对象的构造函数比较复杂耗时。
核心方案:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
设计意图:每当说到创建一个对象实例,我们总是想到调用构造函数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或序列化接口