在介绍其他的设计模式时,为了方便理解,都会引入一些例子,一步一步的进入主题,这是为了方便理解。但原型模式不需要借助案例来帮助理解,因为他足够简单。现在,让我们直接进入主题。
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
结合着意图来看,原型模式的目的在于利用现有的原型创建新的对象,创建的方式为拷贝,所要创建对象的类型由原型实例指定(用原型实例指定创建对象的种类)。
原型模式译自英文'Prototype'一词,该词的中文释义有:原型、样机、样板的涵义。从其翻译的涵义中也可看出这个模式的核心是'复刻',而'复刻'又依赖于现有的'模板'。
原型模式的参与者有如下角色:
- Prototype:声明一个可以克隆自身的行为。可使用接口,也可使用抽象类,可根据实际情况而定。
- ConcretePrototype:实现克隆自身的操作。
在《Design patterns- Elements of reusable object-oriented software》一书中指出,原型模式最困难的部分在于如何正确的实现拷贝操作。尤其是当对象结构包含循环引用时,这尤其棘手。
在 Java 中,提供了拷贝对象的支持,例如克隆。例如Object#clone()
就默认实现了对象的浅拷贝,如果需要实现深拷贝,则需要重写Object#clone()
方法。除此之外,想要克隆对象,类必须实现java.lang.Cloneable
接口。
如果我们想要使用对象的
clone()
方法,就必须让该类实现java.lang.Cloneable
接口,否则在调用clone()
方法时,会得到一个java.lang.CloneNotSupportedException
,但是在编译期并不会得到任何提示。
上面我们已经提到了深拷贝和浅拷贝,如果到此为止你仍然不知道拷贝操作是什么,那么你应该先去查阅资料,因为本章的重点不是介绍拷贝操作,所以不在此阐述什么是拷贝操作。对于深拷贝和浅拷贝,他们之间有什么区别呢?简而言之:
- 对于基本数据类型属性:直接复制其值,对于这些属性的值原对象和拷贝对象各自变化,互不影响;
- 对于引用类型的属性:深拷贝是递归处理,直到遇到基本数据类型。浅拷贝则是直接复制地址引用,因为浅拷贝是复制的地址引用,所以当修改其中一个对象时,另一个对象也会跟着改变(因为他们指向的是同一个对象)。
- 有一些特殊的对象,遵循基本数据类型的原则,比如包装类,借助于不可变机制,能保证原对象和拷贝对象各自隔离。
java.lang.Object#clone()
默认就是浅拷贝的实现,所以,我们可以直接借助于该方法实现,示例代码如下:
public class ShallowClone implements Cloneable {
private int property1;
private String property2;
public void setProperty1(int property1) {
this.property1 = property1;
}
public void setProperty2(String property2) {
this.property2 = property2;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
深拷贝实现方式有很多,这里介绍一种利用 jdk 序列化的方式进行深拷贝。原理是先将原对象进行序列化,再将序列化得到的结果反序列化成为对象,反序列化后对象与原对象互相隔离。
public class DeepClone implements Cloneable, Serializable {
public String val1 = "test";
public Object val2;
@Override
protected Object clone() throws CloneNotSupportedException {
DeepClone target = null;
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
target = (DeepClone) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return target;
}
}
利用序列化实现深拷贝较为简单,但同时也要求该对象必须实现java.io.Serializable
接口,且其中的引用类型的属性也必须实现java.io.Serializable
接口。
- 也可以通过其他第三方工具实现序列化,比如 Jackson 、Gson 等。相比 jdk 实现的序列化,这些工具不要求对象必须实现
java.io.Serializable
接口,更加灵活。- 除此之外,还可以通过在
java.lang.Object#clone()
方法中逐个处理需要深拷贝的属性的方式实现深拷贝,但该方法较为繁琐,需要挨个属性进行考虑,当该属性的类中包含有其他引用类型属性时,还需要递归处理。
现有一家中间商,该中间商经营的业务范围包括有房屋买卖,房屋租售等。当中间商促成买方和卖方进行交易时,会从现有的模板库中调出来标准的房屋买卖合同进行复印,然后由双方进行签字,合同生效;房屋租售业务流程类似。
该案例的类图结构如上所示,类图由以下部分组成。
- AbstractContract:抽象的合同,定义了公用的复制合同
clone():AbstractContract
行为。并且定义了签署合同sign(String,String):void
的行为,两个参数分别是卖方姓名和买方姓名; - SalesContract、LeaseContract:分别代表买卖合同和租售合同。
代码层次及类说明如上所示,更多内容请参考案例代码。客户端示例代码如下
public class Client {
public static void main(String[] args) throws CloneNotSupportedException {
// 实例化原型购房合同
AbstractContract sales = new SalesContract();
// 实例化原型租售合同
AbstractContract lease = new LeaseContract();
System.out.println("|==> 现有[张三]欲购置[李四]的房子-------------------------------------|");
AbstractContract clone4Sales = sales.clone();
clone4Sales.signed("李四", "张三");
System.out.println("|==> 现有[杰克]欲租赁[汤姆]的房子-------------------------------------|");
AbstractContract clone4Lease = lease.clone();
clone4Lease.signed("汤姆", "杰克");
}
}
运行结果如下
|==> 现有[张三]欲购置[李四]的房子-------------------------------------|
复印了一份房屋买卖合同
房屋买方为:张三
房屋出售方为:李四
|==> 现有[杰克]欲租赁[汤姆]的房子-------------------------------------|
复印了一份房屋租售合同
房屋租方为:杰克
房屋出租方为:汤姆
(1)优先浅拷贝
对于原型模式来说,并没有规定必须使用浅拷贝实现或者深拷贝实现。所以在实现时,应根据实际需求出发,灵活调整。在满足需要的前提下,可优先考虑浅拷贝实现,因为浅拷贝实现更加简单。
(2)原型管理器
在开发中,我们常常会给原型模式加一个原型管理器组件,该管理器组件内部维护所有原型的集合,负责初始化所有原型,并且在需要使用新对象的地方调用封装的方法直接获取拷贝对象。如下所示:
public class ContractManager {
private static final Map<String, Contract> CACHE = new HashMap<>();
public static void loadCache() {
Contract sales = new SalesContract();
CACHE.put("sales", sales);
Contract lease = new LeaseContract();
CACHE.put("lease", lease);
}
public static Contract newInstance(String key){
Contract contract = CACHE.get(key);
if (contract == null) {
throw new RuntimeException("不支持的合同类型");
}
return contract.clone();
}
}
在 jdk 的源码中,很多类都实现了 Cloneable 接口,比如java.util.ArrayList
。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
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);
}
}
}