一、介绍
在面向对象系统中,使用原型模式来复制一个对象自身,从而克隆出多个与原型对象一模一样的对象。
另外在软件系统中,有些对象的创建过程较为复杂,而且有时候需要频繁创建,原型模式通过给出一个原型对象来指明所要创建的对象的类型,然后用复制这个原型对象的办法创建出更多同类型的对象,这就是原型模式的意图所在。
1.1 定义
GOF给出的原型模式定义如下:
Specify the kind of objects to create using a prototypical instance, and create new objects by copying this prototype. (使用原型实例指定将要创建的对象类型,通过复制这个实例创建新的对象。)
1.2 模式分析
在原型模式结构中定义了一个抽象原型类,所有的Java类都继承自java.lang.Object,而Object类提供一个clone()方法,可以将一个Java对象复制一份。因此在Java中可以直接使用Object提供的clone()方法来实现对象的克隆,Java语言中的原型模式实现很简单。
能够实现克隆的Java类必须实现一个标识接口Cloneable,表示这个Java类支持复制。如果一个类没有实现这个接口但是调用了clone()方法,Java编译器将抛出一个CloneNotSupportedException异常。
注意: java.lang.Cloneable 只是起到告诉程序可以调用clone方法的作用,它本身并没有定义任何方法。
在使用原型模式克隆对象时,根据其成员对象是否也克隆,原型模式可以分为两种形式:深克隆 和 浅克隆 。
二、实现
下图是原型模式的UML结构图:
下面以一个例子来说明,利用原型模式来模拟复印简历:
//简历类
public class Resume implements Cloneable {
private String name;
private String sex;
private String age;
private WorkExperience work = new WorkExperience();
public Resume(String name) {
this.name = name;
}
public void setPersonalInfo(String sex, String age) {
this.sex = sex;
this.age = age;
}
public void setWorkExperience(String workDate, String company) {
work.setWorkDate(workDate);
work.setCompany(company);
}
public void display() {
System.out.println(name + " " + sex + " " + age);
System.out.println("工作经历:" + work.getWorkDate() + " "
+ work.getCompany());
}
@Override
public Resume clone() throws CloneNotSupportedException {
return (Resume) super.clone();
}
}
//工作经历类
public class WorkExperience {
private String workDate;
private String company;
public String getWorkDate() {
return workDate;
}
public void setWorkDate(String workDate) {
this.workDate = workDate;
}
public String getCompany() {
return company;
}
public void setCompany(String company) {
this.company = company;
}
}
//main方法
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Resume a = new Resume("张三");
a.setPersonalInfo("男", "29");
a.setWorkExperience("1998-2000", "XX公司");
Resume b = a.clone();
b.setWorkExperience("1998-2006", "YY公司");
Resume c = a.clone();
c.setWorkExperience("1998-2003", "ZZ公司");
a.display();
b.display();
c.display();
}
}
输出信息:
张三 男 29
工作经历:1998-2003 ZZ公司
张三 男 29
工作经历:1998-2003 ZZ公司
张三 男 29
工作经历:1998-2003 ZZ公司
这里我们可以看到工作经验都变成了最后更改的信息,原因是WorkExperience是一个类,即引用类型而不是值类型,在复制的时候只是复制了它的引用,只是增加了一个指针指向已存在的内存地址。所以在修改它的时候会导致其它引用到它的地方也会改变。
那么如何解决这个问题呢,我们需要改写Resume 类和 WorkExperience类
//简历类
public class Resume implements Cloneable {
private String name;
private String sex;
private String age;
private WorkExperience work = new WorkExperience();
public Resume(String name) {
this.name = name;
}
public void setPersonalInfo(String sex, String age) {
this.sex = sex;
this.age = age;
}
public void setWorkExperience(String workDate, String company) {
work.setWorkDate(workDate);
work.setCompany(company);
}
public void display() {
System.out.println(name + " " + sex + " " + age);
System.out.println("工作经历:" + work.getWorkDate() + " "
+ work.getCompany());
}
@Override
public Resume clone() {
Resume obj = new Resume(name);
obj.sex = this.sex;
obj.age = this.age;
obj.work = work.clone();
return obj;
}
}
//工作经历类
public class WorkExperience implements Cloneable {
private String workDate;
private String company;
public String getWorkDate() {
return workDate;
}
public void setWorkDate(String workDate) {
this.workDate = workDate;
}
public String getCompany() {
return company;
}
public void setCompany(String company) {
this.company = company;
}
@Override
public WorkExperience clone() {
try {
return (WorkExperience) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
//main方法
public class Main {
public static void main(String[] args) {
Resume a = new Resume("李四");
a.setPersonalInfo("男", "20");
a.setWorkExperience("1998-2000", "XX公司");
Resume b = a.clone();
b.setWorkExperience("1998-2006", "YY公司");
Resume c = a.clone();
c.setPersonalInfo("男", "24");
a.display();
b.display();
c.display();
}
}
输出信息:
李四 男 20
工作经历:1998-2000 XX公司
李四 男 20
工作经历:1998-2006 YY公司
李四 男 24
工作经历:1998-2000 XX公司
我们可以看到三份简历是不同的。
三、 模式优缺点分析
3.1 原型模式的优点:
- 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过一个已有实例可以提高新实例的创建效率。
- 可以动态增加或减少产品类。
- 原型模式提供了简化的创建结构。
- 可以使用深克隆的方式保存对象的状态。
3.2 原型模式的缺点:
- 需要为每一个类配备一个克隆方法,而且这个克隆方法需要对类的功能进行通盘考虑,这对全新的类来说不是很难,但对已有的类进行改造时,不一定是件容易的事,必须修改其源代码,违背了“开闭原则”。
- 在实现深克隆时需要编写较为复杂的代码。
四、原型模式的实际应用案例
4.1 使用场景:
- 资源优化场景。
- 类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。
- 性能和安全要求的场景。
- 通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。
- 一个对象多个修改者的场景。
- 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。
- 在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone 的方法创建一个对象,然后由工厂方法提供给调用者。原型模式已经与 Java 融为浑然一体,大家可以随手拿来使用。
注意事项:与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的。浅拷贝实现 Cloneable,重写,深拷贝是通过实现 Serializable 读取二进制流。
4.2 原型模式的实际应用案例
- 原型模式应用于很多软件中,如果每次创建一个对象要花大量时间,原型模式是最好的解决方案。很多软件提供的复制(Ctrl + C)和粘贴(Ctrl + V)操作就是原型模式的应用,复制得到的对象与原型对象是两个类型相同但内存地址不同的对象,通过原型模式可以大大提高对象的创建效率。
- 在Struts2中为了保证线程的安全性,Action对象的创建使用了原型模式,访问一个已经存在的`Action对象时将通过克隆的方式创建出一个新的对象,从而保证其中定义的变量无须进行加锁实现同步,每一个Action中都有自己的成员变量,避免Struts1因使用单例模式而导致的并发和同步问题。
- 在Spring中,用户也可以采用原型模式来创建新的bean实例,从而实现每次获取的是通过克隆生成的新实例,对其进行修改时对原有实例对象不造成任何影响。