1 概述
原型(Prototype)模式是一种创建型设计模式,其允许通过一个对象创建另外一些可定制的对象,而无需知道任何如何创建的细节。当某个结构复杂的对象由于需求的变化经常面临剧烈的变化,其创建过程也经常需要变化,但是该对象应该对外暴露一个稳定的创建接口,在这种场景下就比较适用原型模式。
原型模式的主要实现方式是:将一个原型对象传递给调用创建接口的对象,该对象通过请求原型对象拷贝自身来实现创建。
2 实现方式
2.1 clone方法
2.1.1 方法使用
说到原型模式的实现,不得不提Java中的clone方法。Java的面向对象特性使得在Java中使用设计模式变得很自然,这一点在很多模式上都有所体现,比如迭代器Iterator和遍历模式的关系。
可以通过调用某个对象的clone方法获得该对象的自身拷贝。clone是Object类的方法,其函数签名为:
protected native Object clone() throws CloneNotSupportedException;
从上述的函数签名可以看出,该方法是一个native方法,被protected修饰并且抛出了CloneNotSupportedException异常。那么什么时候该函数抛出这个异常呢?首先参考一下的例子,有一个描述家庭作业的Homework类,该类封装了完成家庭作业的学生姓名和该作业的得分。在Homework类中,复写了父类Object的clone方法(protected方法只能被同包的类和子类访问)。在测试程序中,调用一个Homework对象的clone方法(表示有人抄袭Tom的家庭作业了)。
public class CloneDemo {
public static void main(String[] args) {
Homework homework = new Homework("Tom", 99);
try {
Homework homeworkCpy = homework.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
class Homework {
private String name;
private int score;
public Homework(String name, int score) {
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
@Override
protected Homework clone() throws CloneNotSupportedException {
return (Homework) super.clone();
}
}
运行上述程序,出现下列异常:
也就是说调用一个普通类的clone方法都会抛出CloneNotSupportedException异常。那么怎样可以顺利的执行clone方法而不抛出异常呢?方法是让该类继承Cloneable接口即可,该接口是标记接口(Mark interface,别的标记接口还有Serializable和RandomAccess等)。也就是将Homework类的签名改为下列方式即可。
class Homework implements Cloneable
2.1.2 clone出来的对象和原对象的关系
既然通过clone方法能得到一个对象,那么该对象和原对象还是一个对象吗,答案是否定的。为了证明这一点,可以将测试程序做如下的修改。有人抄袭了Tom的家庭作业之后,将姓名改成了自己的名字,改完之后我们输出原作业的姓名,发现还是Tom,而抄袭后的作业姓名已经改成了Jack了,说明clone出来的对象和原对象是两个不同的对象。
public class CloneDemo {
public static void main(String[] args) {
Homework homework = new Homework("Tom", 99);
try {
Homework homeworkCpy = homework.clone();
homeworkCpy.setName("Jack");
System.out.println(homework.getName());
System.out.println(homeworkCpy.getName());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
以前在创建一个对象时必须通过new的方式调用构造函数来是实现,而构造函数由于需求的变化可能要经常改变。而通过clone方法可以直接将原对象拷贝一份出来,可以省去复杂且不稳定的创建逻辑,对外提供一个稳定的接口,这就是原型模式的优点。当然,这是要付出代价的,就是被拷贝的类需要实现Cloneable接口。
2.2 浅拷贝和深拷贝
说到clone方法,就不得不提两个非常重要的概念,即浅拷贝和深拷贝。浅拷贝是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象。深拷贝不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象。
那么执行clone方法拷贝对象时是浅拷贝还是深拷贝呢?答案是浅拷贝,为了验证这一点,我们在上面的Homework类中加一个成员变量:Teacher类对象,表示批阅作业的老师,具体参考下列程序:
public class CloneDemo {
public static void main(String[] args) {
Homework homework = new Homework("Tom", 99, new Teacher("Mr Lee", 45));
try {
Homework homeworkCpy = homework.clone();
homeworkCpy.getTeacher().setName("Ms Wang");
System.out.println(homework.getTeacher().getName());
System.out.println(homeworkCpy.getTeacher().getName());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
class Homework implements Cloneable {
private String name;
private int score;
private Teacher teacher;
public Homework(String name, int score, Teacher teacher) {
this.name = name;
this.score = score;
this.teacher = teacher;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
public Teacher getTeacher() {
return teacher;
}
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
@Override
protected Homework clone() throws CloneNotSupportedException {
return (Homework) super.clone();
}
}
class Teacher {
private String name;
private int age;
public Teacher(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
上述程序将输出两个"Ms Wang",这表示当将拷贝的对象的teacher变量的属性改变时,原对象的teacher变量的属性同样发生改变。这就是说,拷贝的对象和原对象的teacher引用指向的是同一个Teacher对象。即clone方法执行的拷贝是浅拷贝。
那么有没有办法利用clone方法进行深拷贝呢?答案是有的,只要让Teacher同样实现Cloneable接口,并复写clone方法,具体参考下列代码:
public class CloneDemo {
public static void main(String[] args) {
Homework homework = new Homework("Tom", 99, new Teacher("Mr Lee", 45));
try {
Homework homeworkCpy = homework.clone();
homeworkCpy.getTeacher().setName("Ms Wang");
System.out.println(homework.getTeacher().getName());
System.out.println(homeworkCpy.getTeacher().getName());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
class Homework implements Cloneable {
private String name;
private int score;
private Teacher teacher;
public Homework(String name, int score, Teacher teacher) {
this.name = name;
this.score = score;
this.teacher = teacher;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
public Teacher getTeacher() {
return teacher;
}
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
@Override
protected Homework clone() throws CloneNotSupportedException {
Homework homework= (Homework) super.clone();
homework.setTeacher(teacher.clone());
return homework;
}
}
class Teacher implements Cloneable {
private String name;
private int age;
public Teacher(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
protected Teacher clone() throws CloneNotSupportedException {
return (Teacher) super.clone();
}
}
在Homework类的clone方法中除了调用super的clone方法拷贝非引用型变量以外,还得调用teacher变量的clone方法。这样,上述程序的执行结果将依次输出"Mr Lee"和"Ms Wang"两行,表示拷贝出来的对象和原对象的teacher引用指向的是不同的对象,即实现了深拷贝。
3 总结
原型模式可以不使用new关键字创建一个对象,创建出来的对象和原对象拥有相同的属性值,原型模式适用于对象的创建过程复杂且不稳定,但需要对外提供一个稳定的创建接口。原型模式在Java中的实践和clone方法紧密相关,而使用clone拷贝对象需要该类实现Cloneable接口。而默认情况下,clone拷贝的动作属于浅拷贝,也就是说拷贝出来的对象的引用型变量指向的对象和原引用指向的对象是同一个。