原型模式的实现,无论是Java还是C#,都是通过克隆(clone)来完成的。虽然具体的实现方式有所不同,但是思想都是一样的:通过调用一个对象实例的克隆方法,创建另一个内部数据与该对象完全相同的实例。原有的对象就是“原型”(prototype)。
换句话说,设计模式意义上的原型模式,其实就是面向对象语言中的对象克隆。
接下来就来看看Java中的对象克隆。可参加《Java核心技术·卷一》6.2节“对象克隆”。
Java的Object类中有一个protected的方法clone,也就是说Java中每一个类都能进行克隆;但是因为它是protected的,所以clone方法只能在当前类的内部使用。
每个类中自带的继承自Object的clone方法默认只能进行“浅拷贝”。所谓“浅拷贝”就是克隆对象中的基本数据类型变量(如int, double, boolean等)和不可改变的引用变量(如String)会具有跟原型对象相同的值,但是其它的引用变量则会与原型对象中的引用变量指向相同的子对象。例如,原型对象的私有成员中有某普通类A的实例a,那么克隆对象中对应的变量同样会引用a;如果在克隆对象中对a进行操作导致a内部的值发生改变,那么原型对象中的a也同样会受影响,因为这两个a是同一个。
与“浅拷贝”对应的“深拷贝”就是不仅克隆对象中的基本数据类型变量(如int, double, boolean等)和不可改变的引用变量(如String)会具有跟原型对象相同的值,而且其它引用对象也会重新实例化与原型对象中的子对象具有相同值的新实例。
如果原型对象中不在乎克隆对普通引用变量的影响,以基础数据类型变量和不可变的应用变量为主,那么直接使用浅克隆会简易很多;但大多数情况下,仍然会用到深克隆以避免原型对象中的子对象被误改。
对于浅拷贝,直接调用clone方法,然后对需要修改的成员变量进行修改即可。下面是一个深拷贝的示例:
public class CloneTest { public static void main(String[] args) { Test t0 = new Test("Test 1"); System.out.println(t0.toString()); Test t1 = t0.clone(); t1.setString("Test 2"); System.out.println(t0.toString()); System.out.println(t1.toString()); System.out.println(t0.toString()); System.out.println(t1.toString()); } } class Test implements Cloneable { private String s; private Counter c; public Test(String s) { this.s = s; this.c = new Counter(); } public void setString(String s) { this.s = s; } public String toString() { c.addOne(); return s + ": " + c.get(); } public Test clone() { Test cloned = null; try { cloned = (Test)super.clone(); cloned.c = new Counter(); cloned.c.set(c.get()); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return cloned; } } class Counter { private int i = 0; public void addOne() { i++; } public int get() { return i; } public void set(int i) { this.i = i; } }
需要注意的是:
1. 根据《Java核心技术》的说法,实现深拷贝的类最好都实现接口Cloneable。Cloneable接口不包含变量和方法,只是一个标记接口,其作用只是做instanceof 逻辑判断的时候表明该类实现了Cloneable接口,进而表明该类能进行克隆。
2. 尽管有时候不是必须的,也仍然要throws或者try…catch异常类CloneNotSupportedException,目的是保证在运行时不会有可能的未处理异常。
3. 在Test类的clone方法中,重新实例化了Counter类,并为它赋上了当前值,这就保证了深拷贝的执行。此处的cloned如果直接调用this.clone() 生成的话,会抛出StackOverflowError,原因不明。按照书中的说法应调用父类(即Object类)的clone方法,再进行类型强制转化。
4. 同样根据书中的说法,克隆过于笨拙,有其它的技术能完全替代它,因此在实际的编程中并不太常用。原型模式也同理。