简介
Specify the kinds of objects to create using a prototypical instance,and create new objects by copying this prototype.
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
原型模式(Prototype Pattern) 是 创建型 设计模式之一,该模式的思想就是将一个对象作为原型,对其进行复制、克隆,产生一个和原对象相似的新对象。
建造者模式 用于创建 复杂对象,而 原型模式 用于创建 初始化耗时较长的对象。
原型模式 的核心在于 拷贝原型对象。以系统中已存在的一个对象为原型,直接基于内存二进制流进行拷贝,无需再经历耗时的对象初始化过程(不调用构造函数),性能提升许多。
主要解决
当对象的构建过程比较耗时时,可以利用当前系统中已存在的对象作为原型,对其进行克隆(基于二进制流的复制),躲避初始化过程,使得新对象的创建时间大大减少。
原型模式优缺点
优点:
- 性能优良,Java自带的 原型模式 是基于内存二进制流的拷贝,比直接
new
一个对象性能上提升了许多。 - 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。
缺点:
- 需要为每一个类配置一个克隆方法,而且该克隆方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违反了开闭原则。
- 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦。
使用场景
- 资源优化:创建对象成本较大(例如初始化时间长,占用CPU多或占太多网络资源)等。
- 性能与安全优化:创建一个对象需要繁琐的数据准备或访问权限等。
- 系统中存在大量多处使用该类对象,且各个调用者都需要修改其值时。
模式讲解
首先看下 原型模式 UML 类图:
从 UML 图中,我们可以看到,原型模式 主要包含三个角色:
- 客户(Client):客户类提出创建对象的请求。
- 抽象原型(Prototype):规定拷贝接口。
- 具体原型(Concrete Prototype):被拷贝的对象。
总结:对不通过new
关键字,而是通过对象拷贝来实现创建对象的模式就称作 原型模式。
代码展示
如果按照上文 原型模式 UML 类图,实现的源码如下所示:
class Client {
//客户类
public static void main(String[] args) {
//创建原型对象
ConcretePrototype type = new ConcretePrototype("original");
System.out.println(type);
//拷贝原型对象
ConcretePrototype cloneType = type.clone();
cloneType.desc = "clone";
System.out.println(cloneType);
}
//抽象原型
interface IPrototype {
T clone();
}
//具体原型
static class ConcretePrototype implements IPrototype {
private String desc;
public ConcretePrototype(String desc) {
this.desc = desc;
}
@Override
public ConcretePrototype clone() {
//进行拷贝
return new ConcretePrototype(this.desc);
}
@Override
public String toString(){
return String.format("[%s--desc: %s]",this.getClass().getSimpleName(),this.desc);
}
}
}
但是在 Java 中,我们无需自己手动去创建 抽象原型 接口,因为 Java 已经内置了 Cloneable 这个 抽象原型 接口,我们自定义的类型,只需实现该接口,并覆写Object.clone() 方法即可完成本类的复制。
我们查看 Cloneable 的 API 文档,可以看到,它是一个空接口。Java 提供的 Cloneable 接口只起一个作用,就是在运行时期通知 Java 虚拟机可以安全地在这个类上使用 clone() 方法。而如果类没有实现 Cloneable 接口,则调用 clone() 方法会抛出 CloneNotSupportedException 异常
一般而言,clone() 方法的实现需满足以下的描述:
- 对任何的对象x,都有:x.clone() != x。换言之,拷贝对象与原对象不是同一个对象。
- 对任何的对象x,都有:x.clone().getClass() == x.getClass(),换言之,拷贝对象与原对象的类型一样。
- 如果对象x的
equals()
方法定义其恰当的话,那么 x.clone().equals(x) 应当成立的。
工程师在设计自定义类的 clone() 方法时,也应当遵守着三个条件。一般来说,上面的三个条件中的前两个是必需的,而第三个是可选的。
下面我们直接使用 Java 提供的 API 改写上文那个 原型模式 实现,代码如下:
class Client {
public static void main(String[] args) {
// 创建原型对象
ConcretePrototype type = new ConcretePrototype("original");
System.out.println(type);
// 拷贝原型对象
ConcretePrototype cloneType = type.clone();
cloneType.desc = "clone";
System.out.println(cloneType);
}
static class ConcretePrototype implements Cloneable {
private String desc;
public ConcretePrototype(String desc) {
this.desc = desc;
}
@Override
protected ConcretePrototype clone() {
ConcretePrototype cloneType = null;
try {
cloneType = (ConcretePrototype) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return cloneType;
}
@Override
public String toString() {
return String.format("[%s--desc: %s]", this.getClass().getSimpleName(), this.desc);
}
}
}
注意:
-
super.clone()
直接是从内存(具体地说是堆内存)中以二进制流的方式进行拷贝,重新分配一个内存块,因此其效率很高(同时,由于是基于内存拷贝,对象的构造函数是不会被调用的,也就是无需经历初始化过程)。 -
super.clone()
进行的是 浅拷贝。如果类中存在引用对象属性,则原型与拷贝对象内该属性指向同一对象。
使用 Java 自带的拷贝机制,clone() 方法进行的是浅拷贝。而如果我们想进行深拷贝,可以直接在super.clone()
后,手动给拷贝对象相关属性分配另一块内存,不过如果原型对象维护很多引用属性的时候,手动分配会比较繁琐。因此,在 Java 中,如果想完成原型对象的深度拷贝,通常使用 序列化(Serializable)的方式,代码如下所示:
public class Client {
public static void main(String[] args) {
// 创建原型对象
ConcretePrototype type = new ConcretePrototype();
System.out.println(type);
// 拷贝原型对象
ConcretePrototype cloneType = type.deepClone();
System.out.println(cloneType);
}
interface IPrototype {
T deepClone();
}
public static class ConcretePrototype implements IPrototype, Serializable {
private static final long serialVersionUID = 1L;
private final List list = new ArrayList();
@Override
public String toString() {
return String.format("list address: %x", System.identityHashCode(this.list));
}
@Override
public ConcretePrototype deepClone() {
ConcretePrototype deepCloneType = null;
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
// 将对象写到流里
oos.writeObject(this);
// 从流里读回来
ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
deepCloneType = (ConcretePrototype) ois.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (ois != null) {
ois.close();
}
if(oos != null){
oos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return deepCloneType;
}
}
}
这里为了不与 Java 自带的 clone() 方法重叠,我们自己按照本文刚开始的自定义 原型模式 方法,自己定义了一个支持深度拷贝的 抽象原型 和 具体原型。
具体原型ConcretePrototype
采用 序列化(Serializable)的方式以流的形式拷贝自己(这里特地将引用变量list
声明成final
类型,为的就是不让自己手动更改指向,强制必须使用 序列化(Serializable)),并且将 具体原型ConcretePrototype
的toString
方法返回内部引用属性的地址,这样,在客户端进行拷贝后,我们通过输出两者的地址空间,就知道是否是同一个对象指向。
代码运行结果如下:
可以看到,我们的确完成了深拷贝。