1、介绍
Specify the kinds of Objects to create using a prototypical instance, and create new objects by copying this prototype.
意思是:用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。
2、使用场景
- 类的初始化需要消耗很多资源,包括数据、硬件等,通过原型拷贝可以避免这种消耗
- 通过new 产生一个对象需要繁琐的数据准备或访问权限,这时也使用原型模式
- 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值,可以考虑使用原型模型拷贝多个对象供调用者使用,即保护性拷贝。
在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过clone()方法创建一个对象,然后由工厂提供给调用者。
注意
- Java中Object提供的clone()方法时浅拷贝,只复制关联对象的引用,而不复制关联对象的数据。
- 通过Cloneable接口实现的原型模式在调用clone()构造实例时不一定比通过new方法快,只有当通过new构造对象耗时耗资源时,clone()方法才会体现效率有所提升。
3、UML类图
角色介绍:
- Client:提出创建对象的请求
- Prototype:抽象类或接口,声明具体clone能力
- ConcretePrototype:具体的原型类,被复制的对象,实现抽象原型接口
4、示例
下面以文档拷贝为例来演示,首先创建一个文档对象WordDocument, 包含文字和图片。用户经过长时间编辑后,打算对文档做进一步编辑,但是不确定编辑的后文档是否被采用。因此,为了安全起见,将当前文档拷贝一份,在副本上进行修改。
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.mImages = this.mImages;
document.mText = this.mText;
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 addImage(String image) {
this.mImages.add(image);
}
public void showWordDocument() {
System.out.println("----showDocument--start--");
System.out.println("Text : " + mText);
for(String img : mImages) {
System.out.println("image name : " + img);
}
System.out.println("----showDocument--end--");
}
}
WordDocument 充当ConcretePrototype角色,Cloneable则为Prototype角色。注意:clone()并不是Cloneable接口中的,而是Object中的方法。Cloneable是一个标识接口,表示这个实现类对象时可拷贝的。若是没有实现Cloneable,而调用了clone方法将会抛出异常。
下面演示Client端的使用:
public class Client {
public static void main(String[] args) {
WordDocument originDoc = new WordDocument();
originDoc.setmText("文档");
originDoc.addImage("图片1");
originDoc.addImage("图片2");
WordDocument copyDoc = (WordDocument) originDoc.clone();
copyDoc.setmText("修改后文档");
System.out.println("----展示修改后的copyDoc内容----");
copyDoc.showWordDocument();
System.out.println("----展示原始originDoc内容----");
originDoc.showWordDocument();
}
}
输出结果如下:
----WordDocument构造函数----
----展示修改后的copyDoc内容----
----showDocument--start--
Text : 修改后文档
image name : 图片1
image name : 图片2
----showDocument--end--
----展示原始originDoc内容----
----showDocument--start--
Text : 文档
image name : 图片1
image name : 图片2
----showDocument--end--
copyDoc是通过originDoc.clone()创建的,copyDoc修改了文本内容不会影响originDoc的文本内容,这保证了originDoc的安全性。注意:通过clone拷贝对象不会执行构造函数。因此,若在构造函数中需要一些特殊的初始化操作,在使用Cloneable实现拷贝是,需要注意构造函数不会执行的问题。
浅拷贝与深拷贝
上述原型模式实际上是一个浅拷贝,也称影子拷贝。并不是将原始文档的所有字段都重新构造了一份,而是副本文档的字段引用原始文档的字段。
public class Client {
public static void main(String[] args) {
WordDocument originDoc = new WordDocument();
originDoc.setmText("文档");
originDoc.addImage("图片1");
originDoc.addImage("图片2");
WordDocument copyDoc = (WordDocument) originDoc.clone();
copyDoc.setmText("修改后文档");
copyDoc.addImage("修改图片");
System.out.println("----展示修改后的copyDoc内容----");
copyDoc.showWordDocument();
System.out.println("----展示原始originDoc内容----");
originDoc.showWordDocument();
}
}
输出结果如下:
----WordDocument构造函数----
----展示修改后的copyDoc内容----
----showDocument--start--
Text : 修改后文档
image name : 图片1
image name : 图片2
image name : 修改图片
----showDocument--end--
----展示原始originDoc内容----
----showDocument--start--
Text : 文档
image name : 图片1
image name : 图片2
image name : 修改图片
----showDocument--end--
从结果中可以看出,最后两个文档的信息输出是一致的。在copyDoc中添加了“修改图片”,originDoc中也会随之更改。因为clone只是浅拷贝,copyDoc中的mImages仅仅指向originDoc的mImage的引用,并没有重新构造一个新的对象,copyDoc与originDoc都是指向同一个对象。深拷贝可以解决这个问题,有两种方式解决:
1) 继续利用clone方法,对其内的引用类型变量再进行clone()。
@Override
protected WordDocument clone() {
try {
WordDocument document = (WordDocument) super.clone();
document.mImages = (ArrayList<String>) this.mImages.clone();
document.mText = this.mText;
return document;
} catch (Exception e) {
}
return null;
}
2) 序列化(serialization)这个对象,再反序列化回来,就可以得到这个新的对象,无非就是序列化的规则需要我们自己来写。
import java.io.Serializable;
import java.util.ArrayList;
public class WordDocument implements Serializable{
private String mText;
private ArrayList<String> mImages = new ArrayList<>();
public WordDocument () {
System.out.println("----WordDocument构造函数----");
}
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 addImage(String image) {
this.mImages.add(image);
}
public void showWordDocument() {
System.out.println("----showDocument--start--");
System.out.println("Text : " + mText);
for(String img : mImages) {
System.out.println("image name : " + img);
}
System.out.println("----showDocument--end--");
}
}
WordDocument需要序列号,实现Serializable。Client端代码如下:
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Client {
public static void main(String[] args) {
try {
WordDocument originDoc = new WordDocument();
originDoc.setmText("文档");
originDoc.addImage("图片1");
originDoc.addImage("图片2");
ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(originDoc);
oos.flush();
ObjectInputStream ois=new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
WordDocument copyDoc = (WordDocument)ois.readObject();
copyDoc.setmText("修改后文档");
copyDoc.addImage("修改图片");
System.out.println("----展示修改后的copyDoc内容----");
copyDoc.showWordDocument();
System.out.println("----展示原始originDoc内容----");
originDoc.showWordDocument();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
上述两种方法一样,输出结果如下:
----WordDocument构造函数----
----展示修改后的copyDoc内容----
----showDocument--start--
Text : 修改后文档
image name : 图片1
image name : 图片2
image name : 修改图片
----showDocument--end--
----展示原始originDoc内容----
----showDocument--start--
Text : 文档
image name : 图片1
image name : 图片2
----showDocument--end--
copyDoc指向的是originDoc.mImages的一份拷贝,所以在copyDoc中添加图片并不会影响原始数据。原型模式的核心问题就是对原始对象进行拷贝,在开发过程中,需要根据具体的使用场景,斟酌使用深拷贝和浅拷贝。
5、总结
优点:
原型模式是内存二进制流的复制,要比直接new 一个对象性能好,特别是在一个循环体内产生大量的对象时,原型模式可以更好地体现其优点。
缺点:
直接在内存中复制,构造函数是不会执行的,虽然减少了约束,在实际开发中需注意这个潜在问题。