一、定义
用原型实例指定创建对象的种类,并通过复制这些原型创建新的对象。
原型模式本质上就是对象拷贝。使用原型模式可以解决构建复杂对象的资源消耗问题,能够在某些场景下提升创建对象的效率。
二、使用场景
- 类初始化需要消耗非常多的资源,包括数据资源、硬件资源。
- 通过new产生一个对象时需要非常繁琐的数据准备和权限访问。
用clone()构造实例时不一定比new要快,所以在使用Cloneable接口时需要考虑。当然,原型模式也不一定非要实现Cloneable接口,也有别的实现方式。
- 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,即保护性拷贝。
三、原型模式的实现
1. 需要拷贝的类
实现Cloneable接口,再去实现clone()方法,实现对象的克隆。
public class WordDocument implements Cloneable {
private String mText;
private ArrayList<String> mImages = new ArrayList<>();
public WordDocument() {
}
@Override
public Object clone() {
try {
WordDocument document = (WordDocument) super.clone();
document.mText = this.mText;
document.mImages = this.mImages;
return document;
} catch (Exception ignored) {
}
return null;
}
public String getText() {
return mText;
}
public void setText(String text) {
mText = text;
}
public ArrayList<String> getImages() {
return mImages;
}
public void addImage(String image) {
mImages.add(image);
}
public void showDocument() {
System.out.println(mText);
for (String image : mImages) {
System.out.println(image);
}
}
}
2. 克隆
尝试一下使用,首先创建一个对象,之后克隆对象再输出。
WordDocument document = new WordDocument();
document.setText("lll");
document.addImage("aaa");
document.addImage("bbb");
document.showDocument();
WordDocument newDocument = (WordDocument) document.clone();
newDocument.showDocument();
newDocument.setText("new change");
newDocument.showDocument();
document.showDocument();
输出结果就是这样,克隆出的newDocument输出和document一样,在修改text后输出,newDocument改变了,document没有改变。
lll
aaa
bbb
lll
aaa
bbb
new change
aaa
bbb
lll
aaa
bbb
3. 深拷贝
看似已经赋值成功了,但是修改输出,就会发现是存在问题的。向克隆出的对象中添加一个Image。
WordDocument document = new WordDocument();
document.setText("lll");
document.addImage("aaa");
document.addImage("bbb");
document.showDocument();
WordDocument newDocument = (WordDocument) document.clone();
newDocument.showDocument();
newDocument.setText("new change");
newDocument.addImage("new");
newDocument.showDocument();
document.showDocument();
输出会发现不仅是克隆出的对象有新Image,原对象也被添加了新Image。这就牵扯到一个深拷贝浅拷贝的概念了。
lll
aaa
bbb
lll
aaa
bbb
new change
aaa
bbb
new
lll
aaa
bbb
new
newDocument并不是将原始对象的所有字段都拷贝了一份,而是直接引用了原始对象的字段。这一句使newDocument的mImages集合指向原对象的mImages,所以在对新对象的集合添加元素时,实际上是在向原始对象集合中添加元素。
document.mImages = this.mImages;
将clone()修改如下,对象的mImages字段也进行clone()。
@Override
public Object clone() {
try {
WordDocument document = (WordDocument) super.clone();
document.mText = this.mText;
document.mImages = (ArrayList<String>) mImages.clone();
return document;
} catch (Exception ignored) {
}
return null;
}
输出如下,就会发现对newDocument添加image时,原始对象不会发生变化。
lll
aaa
bbb
lll
aaa
bbb
new change
aaa
bbb
new
lll
aaa
bbb
所以为了避免会出现上述问题,最好在实现原型模式时尽量使用深拷贝。
在上面实现深拷贝时调用了ArrayList的clone(),顺便来看一下这个实现。
transient Object[] elementData;
private int size;
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);
}
}
首先克隆了自身,再克隆了ArrayList中实际存储数据的数组elementData。由于size是基本类型,并不是对象,所以不需要再克隆。克隆完成后直接返回。
四、Intent中的原型模式
Intent也实现了clone(),可以这样使用。
Intent intent = new Intent(this, MainActivity.class);
startActivity((Intent) intent.clone());
clone()的实现
很出乎意料的是Intent的clone()并没有调用super.clone()。而是调用了构造器并将当前原始Intent作为参数传入。
@Override
public Object clone() {
return new Intent(this);
}
o为原始Intent。
public Intent(Intent o) {
this(o, COPY_MODE_ALL);
}
一直调用到了这个构造器,看到这个拷贝过程完全没有调用clone(),而是使用new的方法拷贝Intent对象的字段。
private Intent(Intent o, @CopyMode int copyMode) {
// 将原始Intent的字段赋值给新Intent
this.mAction = o.mAction;
this.mData = o.mData;
this.mType = o.mType;
// ...
if (o.mCategories != null) {
// 这里重新创建了一个内容一样的ArraySet,深拷贝
this.mCategories = new ArraySet<>(o.mCategories);
}
if (copyMode != COPY_MODE_FILTER) {
// 继续赋值
this.mFlags = o.mFlags;
this.mContentUserHint = o.mContentUserHint;
this.mLaunchToken = o.mLaunchToken;
// 都是通过new的方式达到深拷贝
if (o.mSourceBounds != null) {
this.mSourceBounds = new Rect(o.mSourceBounds);
}
// ...
}
}
所以实现拷贝并不只有clone()一种实现方式,具体使用clone()还是使用new需要看实际情况。当new的成本不高的时候反而会比clone()效率高。
五、小结
- 深拷贝和浅拷贝是原型模式容易出现问题的地方。
- 保护性拷贝是一个重要的用途。
优点
原型模式是对内存中二进制流的拷贝,在某些情况下比直接new一个对象的性能要好很多。
缺点
因为是直接在内存中拷贝,所以不会执行构造函数,在实际开发中应该注意这个潜在的问题。