引言
与单例模式一样也是一种结构简单的创建型,原型模式(Prototype Pattern)的简单程度仅次于单例模式和迭代器模式。正是由于简单,原型模式几乎和Java融为一体了,使用的场景才非常地多,从一定程度上来说原型模式也可以实现降低资源的消耗,提升系统的性能。
一、原型模式(Prototype Pattern)概述
原型模式是属于创建型的一种简单模式,官方定义如下:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。(Specify the kinds of objects to create using a prototypical instance,and create new objects by copying this prototype.)简而言之就是在某些场景通过创建模板的副本来创建新的对象,创建副本的方式主要是通过拷贝,而拷贝又可以分为浅拷贝和深拷贝(具体详情见下文)。
二、原型模式的优点和缺点及常见可用场景
1、原型模式的优点
性能优良,原型模式是在内存二进制流的拷贝,当new的对象比较复杂的时候,要比直接new一个对象性能好很多,特别是在一个循环体内产生大量的对象时,原型模式可以更好的体现其优点。
原型模式允许在运行时动态改变具体的实现类型。原型模式可以在运行期间,由客户来注册符合原型接口的实现类型,也可以动态地改变具体的实现类型,看起来接口没有任何变化,但其实运行的已经是另外一个类实例了。因为克隆一个原型就类似于实例化一个类。
对客户隐藏制造新实例的复杂性
2、原型模式的缺点
由于是直接在内存中拷贝,构造函数不会执行。
原型模式最主要的缺点是每一个类都必须配备一个克隆方法。配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类来说不是很难,而对于已经有的类不一定很容易,特别是当一个类引用不支持序列化的间接对象,或者引用含有循环结构的时候,耦合度较高。
3、适合使用原型模式的场景及注意事项
当通过初始化对象时需要消耗非常多的资源的时候,包括软硬件资源
当通过new 一个对象需要准备繁琐的数据或复杂的权限时
当一个对象需要提供给其他对象访问的时候,而且各个调用者可能会改变其属性的时候可以考虑使用原型模式构造多个副本对象供其他对象调用,又称为保护性拷贝。
使用原型模式时,引用的成员变量必须满足两个条件才不会被拷贝:一是类的成
员变量,而不是方法内变量;二是必须是一个可变的引用对象,而不是一个原始类型或不可
变对象。,即final修饰的不会被拷贝。
三、Java的浅拷贝和深拷贝
总所周知在Java中,万物都是基于java.lang.Object类的,而Object类提供protected Object clone()方法用于对对象的拷贝(当使用Object类的clone()方法来拷贝一个对象时,该对象中拥有其他对象的引用的时候,该对象对其他对象的引用也会被拷贝一份),子类当然也可以把这个方法置换掉,提供满足自己需要的复制方法。除此之外Java中提供了一个用于标识该对象可被拷贝的Cloneable接口,这个接口没有定义任何方法,只是起一个标志作用,implement Cloneable就是为了在运行时期通知JVM可以安全地在这个类上使用clone()方法。由于Object类本身并不实现Cloneable接口,因此如果所拷贝的类没有实现Cloneable接口时,调用clone()方法会抛出CloneNotSupportedException异常。
protected Object clone() throws CloneNotSupportedException {
if (!(this instanceof Cloneable)) {
throw new CloneNotSupportedException("Class " + getClass().getName() +
" doesn't implement Cloneable");
}
return internalClone();
}
/*
* Native helper method for cloning.
*/
private native Object internalClone();
1、拷贝要满足的条件
clone()方法可以将对象复制了一份并返还给调用者,但是和equals方法类似我们在实现clone的时候也应当遵守以下三个条件(其中前两个是必需的,而第三个是可选的):
对任何的对象x,都有——x.clone()!=x。即拷贝对象与原对象不是同一个对象。
对任何的对象x,都有——x.clone().getClass() == x.getClass(),即克隆对象与原对象的类型一样。
如果对象x的equals()方法定义其恰当的话,那么x.clone().equals(x)应当成立的。
2、浅拷贝和深拷贝
实现拷贝的方式总的来说可以有两种:实现系统Cloneable的接口和自己实现拷贝方法,但无论你采用哪种方式,都会存在一个浅度克隆和深度克隆的区别和注意问题。而且深拷贝要深入到多少层,是一个不易确定的问题。在决定以深拷贝的方式拷贝一个对象的时候,必须决定对间接复制的对象时采取浅拷贝还是继续采用深拷贝。此外,在深度克隆的过程中,很可能会出现循环引用的问题,必须小心处理。
2.1、浅拷贝
只负责拷贝按值传递的数据(比如基本数据类型比如int、long、char等都会被拷贝、String类型也会被拷贝),而不复制它所引用的对象(如内部的数组和引用对象地址不拷贝),所有的对其他对象的引用都仍然指向原来的对象)。
2.2、深拷贝
除了浅拷贝要拷贝的值外,还负责拷贝引用类型的数据。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。即把要拷贝的对象所引用的对象都拷贝了一遍,而这种对被引用到的对象的拷贝叫做间接拷贝。
2.3、利用序列化实现深拷贝
在Java中所谓序列化(Serialization)就是把对象写到流里的过程;而反序列化(Deserialization)就是把对象从流中读出来的过程。本质上,写到流里的是对象的一个拷贝操作,而原对象仍然存在于JVM里面。通常在深拷贝一个对象之前,往往先让先对象实现Serializable接口,然后把对象(实际上只是对象的拷贝)写到一个流里(序列化),再从流里读回来(反序列化),便可以重建对象。
public Object 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 ois.readObject();
}
这样做的前提就是对象以及对象内部所有引用到的对象都是可序列化的,否则,就需要仔细考察那些不可序列化的对象可否设成transient,从而将之排除在复制过程之外。浅拷贝显然比深拷贝更容易实现,因为Java语言的所有类都会继承一个clone()方法,而这个clone()方法所做的就是浅拷贝。而有一些对象,比如线程(Thread)对象或Socket对象,是不能简单复制或共享的。不管是使用浅拷贝还是深拷贝,只要涉及这样的间接对象,就必须把间接对象设成transient而不予复制;或者由程序自行创建出相当的同种对象,权且当做复制件使用。
四、原型模式的实现
原型模式要求对象实现一个可以“克隆”自身的接口,这样就可以通过拷贝一个实例对象本身来创建一个新的实例。通过原型实例创建新的对象,也就无须关心这个实例本身的类型,只要实现了克隆自身的方法,就可以通过调用这个方法来获取新的对象而不必通过new来创建。通常原型模式有两种表现形式:简单形式和登记形式
1、原型模式的简单形式
如上图所示,主要角色有三:
抽象原型(Prototype)角色——这是一个抽象角色,可以由一个Java接口或Java抽象类实现,此角色给出所有的具体原型类所需的接口,这个接口可以是自定义的也可以是直接采用Java本身提供的Cloneable
具体原型(Concrete Prototype)角色——被复制的对象。此角色需要实现抽象的原型角色所要求的接口。
客户(Client)角色——使用这个对象的客户类
1.1、采用Cloneable实现简单形式的原型模式
由于Cloneable接口充当了抽象原型角色,所以我们可以直接试下Cloneable接口实现我们自己的具体原型即可
package prototype;
import java.util.ArrayList;
public class WordDocument implements Cloneable {
// 文本
private String mText;
// 图片列表
private ArrayList<String> mImages = new ArrayList<>();
public WordDocument() {
System.out.println("********WordDocument构造方法被调用********");
}
@Override
protected WordDocument clone() {
try {
WordDocument document = (WordDocument) super.clone();
document.mText = this.mText;
document.mImages = this.mImages;//浅拷贝形式
return document;
} catch (Exception e) {
}
return null;
}
public String getmText() {
return mText;
}
public void setmText(String mText) {
this.mText = mText;
}
public ArrayList<String> getmImages() {
return mImages;
}
public void setmImages(ArrayList<String> mImages) {
this.mImages = mImages;
}
public void addImages(String image) {
this.mImages.add(image);
}
// 打印文档内容
public void showDocument() {
System.out.println("Text: " + mText);
System.out.print("Images List: ");
for (String imgName : mImages) {
System.out.print(imgName+"\t");
}
System.out.println("****Word Content End*****\n");
}
}
public class Client{
public static void main(String[] args) {
//构建文档对象
WordDocument originDoc = new WordDocument();
System.out.println("执行拷贝操作之前原始文档的内存地址: "+originDoc.hashCode());//因为hashcode从某种程度上说标示的就是对象的内存地址。
originDoc.setmText("这是一篇最初的文本");
originDoc.addImages("图片1");
originDoc.addImages("图片2");
originDoc.addImages("图片3");
originDoc.showDocument();
System.out.println();
//以原始文档为模板,拷贝一份副本
WordDocument doc2 = originDoc.clone();
System.out.println("执行拷贝操作之后原始文档的内存地址: "+originDoc.hashCode()+"以下是执行了拷贝操作之后还未对副本对象进行初始化时,原始对象的值: ");
originDoc.showDocument();
System.out.println();
System.out.println("执行拷贝操作之后原始文档的内存地址: "+originDoc.hashCode()+"以下是执行了拷贝操作之后还未对副本对象进行初始化时,副本对象的值: ");
doc2.showDocument();
//修改文档副本,不会影响原始文档(仅仅针对非引用字段来说)
doc2.setmText("这是修改过的文本");
//doc2.getmImages().remove(0);
System.out.println("执行拷贝操作之后副本的内存地址: "+doc2.hashCode()+"副本初始化后的值:");
doc2.showDocument();
System.out.println();
System.out.println("执行拷贝操作之后原始文档的内存地址: "+originDoc.hashCode()+"以下是执行了对副本对象进行初始化后,原始对象的值: ");
originDoc.showDocument();
}
}
以上实现的浅拷贝,又被称为影子拷贝,简单理解就是这个拷贝形成副本对象的过程,并不是将原始文档所有引用字段(仅仅针对引用类型字段)都重新构造了一份,而是单纯地使副本中的引用字段的地址指向原始文档,所以就会造成副本中的非引用类型字段相对于原始对象是独立的,而引用字段则是共享一个内存地址,所以修改副本的引用字段,原始对象中对应的引用字段也会跟着改变。
1.2、采用自定义的接口形式
- 自定义自己的接口创建抽象原型(Prototype)角色
public interface MyCloneable {
/**
* 一个从自身克隆出来的对象
*/
public MyCloneable myclone();
}
- 实现自定义的接口创建具体原型(Concrete Prototype)角色
package prototype;
import java.util.ArrayList;
public class WordDocument2 implements MyCloneable {
// 文本
private String mText;
// 图片列表
private ArrayList<String> mImages = new ArrayList<>();
public WordDocument2 myclone(){
WordDocument2 doc2 = new WordDocument2();
doc2.mText = this.mText;
doc2.mImages = this.mImages;
return doc2;
}
//略,同WordDocument2
}
测试使用
public class MainClient2 {
public static void main(String[] args) {
//构建文档对象
WordDocument2 originDoc = new WordDocument2();
System.out.println("执行拷贝操作之前原始文档的内存地址: "+originDoc.hashCode());//因为hashcode从某种程度上说标示的就是对象的内存地址。
originDoc.setmText("这是一篇最初的文本");
originDoc.addImages("图片1");
originDoc.addImages("图片2");
originDoc.addImages("图片3");
originDoc.showDocument();
System.out.println();
//以原始文档为模板,拷贝一份副本
WordDocument2 doc2 = originDoc.myclone();
System.out.println("执行拷贝操作之后原始文档的内存地址: "+originDoc.hashCode()+"以下是执行了拷贝操作之后还未对副本对象进行初始化时,原始对象的值: ");
originDoc.showDocument();
System.out.println();
System.out.println("执行拷贝操作之后原始文档的内存地址: "+originDoc.hashCode()+"以下是执行了拷贝操作之后还未对副本对象进行初始化时,副本对象的值: ");
doc2.showDocument();
//修改文档副本,不会影响原始文档
doc2.setmText("这是修改过的文本");
//doc2.getmImages().remove(0); 这里注释掉是为了避免深拷贝和浅拷贝的影响,如果还不懂的可以把这句执行就会体会到深拷贝的意义了
System.out.println("执行拷贝操作之后副本的内存地址: "+doc2.hashCode()+"副本初始化后的值:");
doc2.showDocument();
System.out.println();
System.out.println("执行拷贝操作之后原始文档的内存地址: "+originDoc.hashCode()+"以下是执行了对副本对象进行初始化后,原始对象的值: ");
originDoc.showDocument();
}
}
2、原型模式的登记形式
所谓原型模式的登记形式,本质上结构和简单模式大同小异,只不过登记形式中多了一个用于管理原型的“登记者”——创建具体原型类的对象,并记录每一个被创建的对象。
简而言之,采用”登记形式”时,第一步还是先创建原型模板对象,然后把该对象注册到“登记者”管理类中,再通过“登记者”的方法获得拷贝的对象,再不需要使用的时候可以取消注册。
- 实现抽象角色,与简单形式不同这个抽象角色除了需要定义clone方法,还需要定义其他共性的方法,以便于在使用的时候访问或修改属性。
public interface MyCloneable2 {
public MyCloneable2 myclone();
//定义一些具体原型所共有的操作
public String getName();
public void setName(String name);
}
- 实现具体角色
//假设new 创建Document2 需要消耗大量的资源,Document1代码略和Document2类似
public class Document2 implements MyCloneable2 {
private String mText;
@Override
public MyCloneable2 myclone() {
Document2 doc2 = new Document2();
doc2.setName(mText);
return doc2;
}
public String toString() {
return "Now in Prototype1 , name = " + this.mText;
}
@Override
public String getName() {
return mText;
}
@Override
public void setName(String name) {
this.mText = name;
}
}
- 实现登记者角色,用于创建具体原型的类的对象并记录每一个被创建的对象
public class DocPrototypeManager {
/**
* 用来记录原型的编号和原型实例的对应关系
*/
private static Map<String,MyCloneable2> map = new HashMap<String,MyCloneable2>();
/**
* 私有化构造方法,避免外部创建实例
*/
private DocPrototypeManager(){}
/**
* 向原型管理器里面添加或是修改某个原型注册
* @param MyCloneable2Id 原型编号
* @param MyCloneable2 原型实例
*/
public synchronized static void setMyCloneable2(String MyCloneable2Id , MyCloneable2 MyCloneable2){
map.put(MyCloneable2Id, MyCloneable2);
}
/**
* 从原型管理器里面删除某个原型注册
* @param MyCloneable2Id 原型编号
*/
public synchronized static void removeMyCloneable2(String MyCloneable2Id){
map.remove(MyCloneable2Id);
}
/**
* 获取某个原型编号对应的原型实例
* @param MyCloneable2Id 原型编号
* @return 原型编号对应的原型实例
* @throws Exception 如果原型编号对应的实例不存在,则抛出异常
*/
public synchronized static MyCloneable2 getMyCloneable2(String MyCloneable2Id) throws Exception{
MyCloneable2 MyCloneable2 = map.get(MyCloneable2Id);
if(MyCloneable2 == null){
throw new Exception("您希望获取的原型还没有注册或已被销毁");
}
return MyCloneable2;
}
}
测试
public class MainRegistClient {
public static void main(String[] args) {
try {
MyCloneable2 doc1 = new Document1();
DocPrototypeManager.setMyCloneable2("doc1", doc1);
// 获取原型来创建对象
MyCloneable2 doc3 = DocPrototypeManager.getMyCloneable2("doc1").myclone();
doc3.setName("张三");
System.out.println("第一个实例:" + doc3);
// 有人动态的切换了实现
MyCloneable2 doc2 = new Document2();
DocPrototypeManager.setMyCloneable2("doc1", doc2);
// 重新获取原型来创建对象
MyCloneable2 doc4 = DocPrototypeManager.getMyCloneable2("doc1").myclone();
doc4.setName("李四");
System.out.println("第二个实例:" + doc4);
// 有人注销了这个原型
DocPrototypeManager.removeMyCloneable2("doc1");
// 再次获取原型来创建对象
MyCloneable2 doc5 = DocPrototypeManager.getMyCloneable2("doc1").myclone();
doc5.setName("王五");
System.out.println("第三个实例:" + doc5);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果
第一个实例:Now in Prototype1 , name = 张三
第二个实例:Now in Document2 , name = 李四
java.lang.Exception: 您希望获取的原型还没有注册或已被销毁
at prototype.registtype.DocPrototypeManager.getMyCloneable2(DocPrototypeManager.java:39)
at prototype.registtype.MainRegistClient.main(MainRegistClient.java:25)
以上代码均是基于浅拷贝实现的,以WordDocument 为例,拷贝的时候并不是将原始文档中的所有引用字段重新构造一份副本,而是将副本中对应的引用字段的引用指向原始文档中对应的字段,把我上面的代码中的doc2.getmImages().remove(0)执行一遍就可验证。所以当原始对象中存在引用字段的时候我们应该采用深拷贝的方式,只需要把myclone方法改造一下即可:
@Override
protected WordDocument clone() {
try {
WordDocument document = (WordDocument) super.clone();
document.mText = this.mText;
//对于引用字段也调用clone进行copy
document.mImages = (ArrayList<String>) this.mImages.clone();
return document;
} catch (Exception e) {
}
return null;
}
深拷贝还有一种实现方式就是前面提动的通过流的方式。
3、简单形式和登记形式的简单比较
简单形式和登记形式的原型模式各有其长处和短处。如果需要创建的原型对象数目较少而且比较固定的话,可以采取第一种形式。在这种情况下,原型对象的引用可以由客户端自己保存。如果要创建的原型对象数目不固定的话,可以采取第二种形式。在这种情况下,客户端不保存对原型对象的引用,这个任务被交给管理员对象。在复制一个原型对象之前,客户端可以查看管理员对象是否已经有一个满足要求的原型对象。如果有,可以直接从管理员类取得这个对象引用;如果没有,客户端就需要自行复制此原型对象。