定义:
给出一个原型对象实例来指定创建对象的类型,并通过拷贝这些原型的方式来创建新的对象。
原型模式是简单程度仅次于单例模式的简单模式,它的定义可以简单理解为对象的拷贝,通过拷贝的方式创建一个已有对象的新对象,这就是原型模式。
设计类图:
在原型模式中主要的任务是实现一个接口,这个接口具有一个clone方法可以实现拷贝对象的功能,也就是上图中的ProtoType接口。由于在Java语言中,JDK已经默认给我们提供了一个Coneable接口,所以我们不需要手动去创建ProtoType接口类了。Coneable接口在java中是一个标记接口,它并没有任何方法,只有实现了Coneable接口的类在JVM当中才有可能被拷贝。既然Coneable接口没有任何方法,那clone方法从哪里来呢?由于在java中所有的类都是Object类的子类,所以我们只需要重写来自Object类的clone方法就可以了。
示例代码如下:
public class ProtoTypeClass implements Cloneable {
@Override
protected ProtoTypeClass clone() throws CloneNotSupportedException {
return (ProtoTypeClass)super.clone();
}
}
public class Client {
public static void main(String[] args) {
ProtoTypeClass protoType = new ProtoTypeClass();
try {
//通过clone生成一个ProtoTypeClass类型的新对象
ProtoTypeClass cloneObject = protoType.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
看吧,原型模式的代码简直不要太简单,简单到不能再简单了,只需要实现Cloneable接口,然后覆写clone方法,调用super.clone就可以实现简单的对象复制了。
值得注意的是,使用clone方法创建的新对象的构造函数是不会被执行的,也就是说会绕过任何构造函数(有参和无参),因为clone方法的原理是从堆内存中以二进制流的方式进行拷贝,直接分配一块新内存。
深拷贝和浅拷贝
浅拷贝
浅拷贝只会拷贝对象本身相关的基本类型数据,直接看示例代码:
public class EasyCopyExample implements Cloneable {
private List<String> nameList = new ArrayList<>();
@Override
protected EasyCopyExample clone() throws CloneNotSupportedException {
return (EasyCopyExample) super.clone();
}
public void addName(String name) {
nameList.add(name);
}
public void printNames() {
for (String name : nameList) {
System.out.println(name);
}
}
}
public class Client {
public static void main(String[] args) {
try {
//创建一个原始对象并添加一个名字
EasyCopyExample originalObject = new EasyCopyExample();
originalObject.addName("test1");
//克隆一个新对象并添加一个名字
EasyCopyExample cloneObject = originalObject.clone();
cloneObject.addName("test2");
//打印原始对象和新对象的name
originalObject.printNames();
System.out.println();
cloneObject.printNames();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
输出结果:
可以看到通过clone()创建的新对象与之前的对象都打印出了相同的name, 这说明新对象与原始对象是共用nameList的这个成员变量的,这就是浅拷贝,拷贝之后的对象会和原始对象共用一部分数据,这样会给使用上带来困扰,因为一个变量不是静态的但却可以多个对象同时修改它的值。在java中除了基本数据类型(int long等)和String类型,数组引用和对象引用的成员变量都不会被拷贝。
深拷贝
为了避免上面的情况,我们就需要对具有clone方法不支持拷贝的数据的对象自行处理,将上述示例代码修改如下:
public class DeepCopyExample implements Serializable {
private List<String> nameList = new ArrayList<>();
private String name = "张三";
private int age = 23;
public DeepCopyExample deepClone() throws IOException, ClassNotFoundException {
//将对象写到流里
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
//从流里读回来
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (DeepCopyExample) ois.readObject();
}
public void addName(String name) {
nameList.add(name);
}
public void printNames() {
for (String name : nameList) {
System.out.println(name);
}
System.out.println(name);
System.out.println(age);
}
}
public class Client {
public static void main(String[] args) {
try {
//创建一个原始对象并添加一个名字
DeepCopyExample originalObject = new DeepCopyExample();
originalObject.addName("test1");
//克隆一个新对象并添加一个名字
DeepCopyExample cloneObject = originalObject.deepClone();
cloneObject.addName("test2");
//打印原始对象和新对象的name
originalObject.printNames();
System.out.println("-----------");
cloneObject.printNames();
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出结果:
上面的代码是通过序列化和反序列化的方式实现对象的拷贝的,通过实现Serializable接口,对象可以写到一个流里(序列化),再从流里读回来(反序列化),便可以重建对象。可以看到输出结果中新对象保留了原始对象的基本类型数据(name和age),同时针对新对象操作List数据不会影响原始对象,这说明跟原始对象是完全隔离开了,是两个完全独立的对象。
能够使用这种方式做的前提是,对象以及对象内部所有引用到的对象都是可序列化的,否则,就需要仔细考察那些不可序列化的对象可否设成transient,从而将之排除在复制过程之外。
有一些对象,比如线程(Thread)对象或Socket对象,是不能简单复制或共享的。不管是使用浅度克隆还是深度克隆,只要涉及这样的间接对象,就必须把间接对象设成transient而不予复制;或者由程序自行创建出相当的同种对象。
原型模式的优缺点
优点很明显就是可以绕过繁琐的构造函数,快速创建对象,且比直接new一个对象性能优良,因为是直接内存二进制流拷贝。原型模式非常适合于你想要向客户隐藏实例创建的创建过程的场景,提供客户创建未知类型对象的选择。
原型模式最主要的缺点是每一个类都必须配备一个克隆方法。配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类来说不是很难,而对于已经有的类不一定很容易,特别是当一个类引用不支持序列化的间接对象,或者引用含有循环结构的时候。
参考: