原型模式是一种较为简单的设计模式
描述
原型模式是一个创建型的模式,原型就是可供复制的模板,通过复制原型得到一个新的实例,这个过程我们俗称为“克隆”。被复制的实例就是我们所说的“原型”。
什么地方比较适合使用?
- 类初始化时需要消耗较多的资源,包括数据,硬件资源等,通过拷贝避免这些消耗。
- 通过new产生一个对象需要非常繁琐的数据准备或访问权限。
- 需要大量相同的对象
简单实现
我们有一个文档类,类中包含标题和文字如下:
class Document implements Cloneable{
private String mName;
private List<String> mTexts = new ArrayList<>();
public String getmName() {
return mName;
}
public void setmName(String mName) {
this.mName = mName;
}
public List<String> getmTexts() {
return mTexts;
}
public void addMtexts(String mTexts) {
this.mTexts.add(mTexts);
}
public void setMtexts(List<String> mTexts) {
this.mTexts = mTexts;
}
@Override
protected Document clone() {
// TODO Auto-generated method stub
try {
Document document = (Document) super.clone();
document.mName = this.mName;
document.mTexts = this.mTexts;
return document;
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
@Override
public String toString() {
return "Document [mName=" + mName + ", mTexts=" + mTexts + "]";
}
}
上面时克隆模式的一种简单实现。我们通过实现Cloneable接口和覆写clone方法来实现对象的拷贝,需要注意的时clone方法并不是Cloneable接口中的,而是Object中的方法,Cloneable只是一个标识接口,用来表明该类是可以拷贝的,而拷贝的具体实现是在clone方法中的(如果没有实现Cloneable接口直接调用clone方法将会抛出异常)。
下面我们来试用一下卡隆模式:
public static void main(String[] args) {
// TODO Auto-generated method stub
Document document = new Document();
document.setmName("文档");
document.addMtexts("文档1");
document.addMtexts("文档2");
document.addMtexts("文档3");
System.out.println(document.toString());
//生成拷贝
Document documentClone = document.clone();
documentClone.setmName("克隆");
System.out.println(document.toString());
System.out.println(documentClone.toString());
}
输出:
Document [mName=文档, mTexts=[文档1, 文档2, 文档3]]
Document [mName=文档, mTexts=[文档1, 文档2, 文档3]]
Document [mName=克隆, mTexts=[文档1, 文档2, 文档3]]
从上面的输出可以看出,通过clone方法确实时复制了一份document对象,里面的文字内容是一样的,我们修改了documentClone对象的name,所以我们看到它改变了,它并不会影响document将对象中的name值。
克隆模式真的这么简单吗?如果我们修改documentClone中文字会怎样呢?
运行结果:
Document [mName=文档, mTexts=[文档1, 文档2, 文档3]]
Document [mName=文档, mTexts=[文档1, 文档2, 文档3, 追加文档]]
Document [mName=克隆, mTexts=[文档1, 文档2, 文档3, 追加文档]]
从结果中我们可以看出修改documentClone对象中的内容document对象中的内容也跟着需改了,这是什么情况?下面我们就来聊一聊这个问题。
浅拷贝与深拷贝
上面的例子中我们实际上实现的只是一个浅拷贝,这份拷贝实际上并不是将原始数据所有字段都重新构造了一份,而是副本中的字段引用指向原始文档中的字段,如下:
A引用B就是说两个对象指向同一个地址,当修改A时B也会发生改变,同时修改B时A也会跟着变化。
那如何修改这种浅拷贝呢(变为深拷贝)?
我们需要重写clone方法来达到我们的目的,如下:
@Override
protected Document clone() {
// TODO Auto-generated method stub
try {
Document document = (Document) super.clone();
document.mName = this.mName;
document.mTexts = (ArrayList)this.mTexts.clone();
return document;
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
mTexts 对象是一份拷贝,而不是一个地址指向,这就实现了深度拷贝,我们再来运行一下,运行结果如下:
Document [mName=文档, mTexts=[文档1, 文档2, 文档3]]
Document [mName=文档, mTexts=[文档1, 文档2, 文档3]]
Document [mName=克隆, mTexts=[文档1, 文档2, 文档3, 追加文档]]
我们会发现再修改documentClone对象,不再对document对象产生影响了。这归功于ArrayList类中实现了Cloneable接口并重写了clone方法,内部实现如下:
需要强调一点的是:实现cloneable和复写clone方法只是原型模式的一种实现方式。