1. 简介
原型模式又称克隆模式,它允许一个对象可以创建出另一个与自身类型相同的对象。客户不需要知道创建的细节,就可以获取到需要的复制品。
在java中通过继承Cloneable的接口实现原型模式,在c++中则可以通过重载赋值函数进行实现。
在实现原型模式的时候,我们会根据实际情况和业务模型对其做不同的实现,一般分为浅拷贝和深拷贝两种方式来对原对象进行复制,从而创建出一个新的对象。
2. 类图
原型模式的设计类图很简单,它需要一个原型类,继承Cloneable,通过实现接口中的clone方法来实现对原型的对象创建。如下所示:
3. 实例
首先,我们创建一个原型发票类,然后实现发票的clone方法,从而复制出另一张发票对象。
3.1 原型类
3.1.1 发票类:
class EInvoice implements Cloneable {
private String strName;
private String strCompanyName;
private String strId;
private String strDate;
private float fAmount;
public EInvoice clone() {
EInvoice invoice = null;
try {
invoice = (EInvoice)super.clone();
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return invoice;
}
public String getName() {
return this.strName;
}
public void setName(String strName) {
this.strName = strName;
}
}
3.1.2 客户调用:
EInvoice invoice= new EInvoice();
invoice.setName("张三");
EInvoice invoiceClone = invoice.clone();
System.out.println(invoiceClone.getName());
如上,发票类继承了Cloneable接口,然后通过实现clone方法来实现对自身类型的对象创建。这里直接调用父类的clone方法,进行向下类型转换完成发票对象的创建。父类的克隆方法属于浅拷贝的方式。
3.2 浅拷贝
浅拷贝,顾名思义,就是拷贝一些面子上的东西,而没有拷贝实质的里子。即当一个对象内部足够复杂时,例如内部有其他类型的实例A(引用数据类型),如果进行浅拷贝的话,实际上只是拷贝了这个对象的引用,即拷贝出来的对象中的A实例与原对象中A实例指向了同一块内存,如果我们改变了原对象中实例A的内容,拷贝后的对象中的实例A的内容也会发生同样的变化,这点在C++中通常是指针的拷贝,而非指针所指向的内存拷贝。
3.2.1 发票集合类
class EInvoiceCollection implements Cloneable {
private ArrayList<EInvoice> invoiceList;
public EInvoiceCollection() {
invoiceList = new ArrayList<EInvoice>();
}
public void addInvoice(EInvoice invoice) {
invoiceList.add(invoice);
}
public EInvoiceCollection clone() {
EInvoiceCollection invoiceCollection = null;
try {
invoiceCollection = (EInvoiceCollection)super.clone();
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return invoiceCollection;
}
public void printAllInvoiceName() {
if (this.invoiceList == null) {
return;
}
int nSize = this.invoiceList.size();
for (int i = 0; i < nSize; i++) {
String strName = this.invoiceList.get(i).getName();
System.out.println(strName);
}
}
}
如上,是一个发票集合类,内部含有一个ArrayList集合,同样对其进行了继承Cloneable接口,然后实现clone方法。通过客户代码调用和输出,验证一下其是否为浅拷贝:
public class EPrototype {
public static void main(String[] args) {
// TODO Auto-generated method stub
EInvoice invoice= new EInvoice();
invoice.setName("张三");
EInvoice invoiceClone = invoice.clone();
System.out.println(invoiceClone.getName());
EInvoiceCollection invoiceCollection = new EInvoiceCollection();
for (int i = 0; i < 3; i++) {
invoiceCollection.addInvoice(invoice.clone());
}
EInvoiceCollection invoiceCollClone = invoiceCollection.clone();
invoiceClone.setName("李四");
invoiceCollClone.addInvoice(invoiceClone);
System.out.println("原型发票集合:");
invoiceCollection.printAllInvoiceName();
System.out.println("浅拷贝后发票集合:");
invoiceCollClone.printAllInvoiceName();
}
}
输出结果如下:
张三
原型发票集合:
张三
张三
张三
李四
浅拷贝后发票集合:
张三
张三
张三
李四
根据输出结果,可以看到,浅拷贝和原型的发票集合的结果是一致的。也就是对拷贝后的发票集合新增一个发票,原型也跟着新增了一个发票。这就是浅拷贝,两个对象中的ArrayList是同一个,拷贝出来的对象拥有ArrayList的引用,所以改变拷贝出来的对象中的ArrayList,其实就是改变原型中的ArrayList。在一些特殊的业务场景中,浅拷贝是很有用的,但使用者一定要知道其是个浅拷贝,专门服务于业务而做的设计。
3.3 深拷贝
有浅拷贝,对应的就有深拷贝,深拷贝其实就是将引用类型所指向的内容拷贝到另一个对象中,这两个对象除了类型一致外,他们所占用的内存地址是不一样的。对两者中的任意一个修改都不会影响到另外一个。
一般的,如果一个类很复杂,内部有很多引用类型的成员,我们会禁用深拷贝。但是对于类不是很复杂,内部有少量的引用类型的成员,如果业务有需求,可以提供深拷贝的方法,这些都是根据业务需求来变化。现提供两种深拷贝的方法。
3.3.1 穷举调用成员的clone方法
// Deep cloning
class EInvoiceCollection implements Cloneable {
ArrayList<EInvoice> invoiceList;
public EInvoiceCollection() {
invoiceList = new ArrayList<EInvoice>();
}
public void addInvoice(EInvoice invoice) {
invoiceList.add(invoice);
}
public EInvoiceCollection clone() {
EInvoiceCollection invoiceCollection = null;
try {
invoiceCollection = (EInvoiceCollection)super.clone();
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
invoiceCollection.invoiceList = (ArrayList<EInvoice>) this.invoiceList.clone();
return invoiceCollection;
}
public void printAllInvoiceName() {
if (this.invoiceList == null) {
return;
}
int nSize = this.invoiceList.size();
for (int i = 0; i < nSize; i++) {
String strName = this.invoiceList.get(i).getName();
System.out.println(strName);
}
}
}
还是调用之前的客户代码,输出结果:
张三
原型发票集合:
张三
张三
张三
浅拷贝后发票集合:
张三
张三
张三
李四
如果内部的引用类型的对象类本身也提供了深拷贝的方法,就可以在聚合类中一个个的调用成员的Clone方法实现深拷贝,显然这种方法是比较麻烦的,有些引用类型成员没有clone方法就不行了,且引用类型成员多的话,也会容易出错,维护麻烦。
3.3.2 序列化对象
interface ECloneable extends Cloneable, Serializable {
}
class EInvoice implements ECloneable {
private String strName;
private String strCompanyName;
private String strId;
private String strDate;
private float fAmount;
public EInvoice clone() {
EInvoice invoice = null;
try {
invoice = (EInvoice)super.clone();
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return invoice;
}
public String getName() {
return this.strName;
}
public void setName(String strName) {
this.strName = strName;
}
}
class EInvoiceCollection implements ECloneable {
ArrayList<EInvoice> invoiceList;
public EInvoiceCollection() {
invoiceList = new ArrayList<EInvoice>();
}
public void addInvoice(EInvoice invoice) {
invoiceList.add(invoice);
}
public EInvoiceCollection clone() {
EInvoiceCollection invoiceCollection = null;
ByteArrayOutputStream byteOS = new ByteArrayOutputStream();
ObjectOutputStream objectOS;
try {
objectOS = new ObjectOutputStream(byteOS);
objectOS.writeObject(this);
} catch (IOException e) {
e.printStackTrace();
}
ByteArrayInputStream byteIS = new ByteArrayInputStream(byteOS.toByteArray());
ObjectInputStream objectIS;
try {
objectIS = new ObjectInputStream(byteIS);
invoiceCollection = (EInvoiceCollection) objectIS.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return invoiceCollection;
}
public void printAllInvoiceName() {
if (this.invoiceList == null) {
return;
}
int nSize = this.invoiceList.size();
for (int i = 0; i < nSize; i++) {
String strName = this.invoiceList.get(i).getName();
System.out.println(strName);
}
}
}
客户代码仍旧不变,输出结果如下:
张三
原型发票集合:
张三
张三
张三
浅拷贝后发票集合:
张三
张三
张三
李四
如上,通过继承Serializable接口在clone方法中使用字节流将对象先写入对象输出流,这一过程其实就是拷贝原型的堆内存中的内容到另个内存中,然后再转换回原型类型,实现深拷贝。
4. 总结
原型模式相对比较简单,理解了浅拷贝和深拷贝就会很好的利用原型模式,如果你对c++中的浅拷贝和深拷贝熟悉些,那么对java中的浅拷贝和深拷贝就不难理解了。