目录
本文的结构如下:
- 引言
- 什么是原型模式
- 浅克隆和深克隆
- clone()
- 模式的结构
- 典型代码
- 代码示例
- 优点和缺点
- 适用环境
一、引言
我是一个很幼稚的人,所以经常会有很幼稚的想法,比如,有时候上着班我就在想,要是我能够影分身多好,这样,我可以让一号分身陪女朋友和家人,二号分身上班敲代码,三号分身街头卖烤串,四号分身被窝玩游戏......
我的可笑想法在当下是不现实的,但在软件开发中,却是非常务实的。设计模式中有一个模式,可以通过一个原型对象克隆出多个一模一样的对象,该模式称之为原型模式。
二、什么是原型模式
在使用原型模式时,我们需要首先创建一个原型对象,再通过复制这个原型对象来创建更多同类型的对象。一般这个原型对象的实例化很复杂,需要消耗很多的硬件资源或者数据资源,就像引言中说的“我”,是经过20多年的实例化,消耗了大量的粮食才构造成功的。
原型模式(Prototype Pattern):当创建给定类的实例化过程很复杂或者代价很昂贵时,可以使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式是一种对象创建型模式。
原型模式的工作原理很简单:将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝自己来实现创建过程,拷贝通常是通过克隆方法实现。原型模式是一种“另类”的创建型模式,创建克隆对象的工厂就是原型类自身,工厂方法由克隆方法来实现。
三、浅克隆和深克隆
需要注意的是克隆有深克隆和浅克隆之分。
浅克隆: 在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。
深克隆: 在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。
四、clone()
4.1、clone()方法理解
clone()方法是Object中的一个方法,其源代码如下:
protected native Object clone() throws CloneNotSupportedException;
可以发现:
- 第一:Object类的clone()方法是一个native方法,native方法的效率一般来说都是远高于Java中的非native方法。这也解释了为什么要用Object中clone()方法而不是先new一个类,然后把原始对象中的信息复制到新对象中,虽然这也实现了clone功能。
JNI是Java Native Interface的 缩写。从Java 1.1开始,Java Native Interface(JNI)标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍你使用其他语言,只要调用约定受支持就可以了。使用java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的,比如,使用一些旧的库,与硬件、操作系统进行交互,或者为了提高程序的性能。JNI标准至少保证本地代码能工作在任何Java虚拟机实现下。
- 第二:Object类中的clone()方法被protected修饰符修饰。这也意味着如果要应用clone()方法,必须继承Object类,在Java中所有的类是缺省继承Object类的,也就不用关心这点了。然后重载clone()方法。还有一点要考虑的是为了让其它类能调用这个clone类的clone()方法,重载之后要把clone()方法的属性设置为 public。
- 第三:Object.clone()方法返回一个Object对象。必须进行强制类型转换才能得到需要的类型。
4.2、Java中对象的克隆
一般需要四个步骤:
- 在子类中实现Cloneable接口。
- 为了获取对象的一份拷贝,我们可以利用Object类的clone方法。
- 在子类中覆盖clone方法,声明为public。
- 在子类的clone方法中,调用super.clone()。
Cloneable接口仅仅是一个标志接口,而且这个标志也仅仅是针对Object类中 clone()方法的,如果clone类没有实现Cloneable接口,并调用了Object的clone()方法(也就是调用了super.Clone()方法),那么Object的clone()方法就会抛出 CloneNotSupportedException 异常。
4.3、浅克隆实例
public class Student implements Cloneable{
private String name;
private int age;
private Professor professor;
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;
}
public Professor getProfessor() {
return professor;
}
public void setProfessor(Professor professor) {
this.professor = professor;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + ", professor="
+ professor + "]";
}
public Object clone() throws CloneNotSupportedException{
return super.clone();
}
}
public class Professor {
private String name;
private int 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
public String toString() {
return "Professor [name=" + name + ", age=" + age + "]";
}
}
public class ShadowCopy {
public static void main(String[] args) {
Professor p1 = new Professor();
p1.setName("Professor Zhang");
p1.setAge(30);
Student s1 = new Student();
s1.setName("xiao ming");
s1.setAge(18);
s1.setProfessor(p1);
System.out.println(s1);
try {
Student s2 = (Student) s1.clone();
Professor p2 = s2.getProfessor();
p2.setName("Professor Li");
p2.setAge(45);
s2.setProfessor(p2);
System.out.println("复制后的:s1 = " + s1);
System.out.println("复制后的:s2 = " + s2);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
测试结果会发现复制后打印出来的s1和s2结果是一样的,s1和s2的导师都变成了45岁的Professor Li,显然这并不是期望的结果,产生这个结果的原因lone()方法实现的是浅克隆,对象引用professor只是复制了其引用,s1和s2仍是指向相同的地址块。
4.4、深克隆实例
实现深克隆,可以在原clone()方法基础上改进一下,如下:
public class Student implements Cloneable{
private String name;
private int age;
private Professor professor;
public Student(String name, int age, Professor professor){
this.name = name;
this.age = age;
this.professor = professor;
}
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;
}
public Professor getProfessor() {
return professor;
}
public void setProfessor(Professor professor) {
this.professor = professor;
}
public Object clone(){
Student o = null;
try {
o = (Student)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
o.professor = (Professor)professor.clone();
return o;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + ", professor="
+ professor + "]";
}
}
public class Professor implements Cloneable{
private String name;
private int age;
public Professor(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
public Object clone(){
Object o = name;
try {
o= super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return o;
}
@Override
public String toString() {
return "Professor [name=" + name + ", age=" + age + "]";
}
}
public class DeepClone {
public static void main(String[] args) {
Professor p=new Professor("wangwu",50);
Student s1=new Student("zhangsan",18, p);
System.out.println(s1);
Student s2=(Student)s1.clone();
s2.getProfessor().setName("maer");
s2.getProfessor().setAge(40);
System.out.println("复制后的:s1 = " + s1);
System.out.println("复制后的:s2 = " + s2);
}
}
也可以利用序列化反序列化来实现深克隆:
public class Student implements Serializable {
private String name;
private int age;
private Professor professor;
public Student(String name, int age, Professor professor){
this.name = name;
this.age = age;
this.professor = professor;
}
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;
}
public Professor getProfessor() {
return professor;
}
public void setProfessor(Professor professor) {
this.professor = professor;
}
public Object deepClone() throws IOException, ClassNotFoundException {
//将对象写到流中
ByteArrayOutputStream bo=new ByteArrayOutputStream();
ObjectOutputStream oo=new ObjectOutputStream(bo);
oo.writeObject(this);
//从流中读出来
ByteArrayInputStream bi=new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream oi=new ObjectInputStream(bi);
return oi.readObject();
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + ", professor="
+ professor + "]";
}
}
public class Professor implements Serializable {
private String name;
private int age;
public Professor(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
public String toString() {
return "Professor [name=" + name + ", age=" + age + "]";
}
}
public class DeepClone {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Professor p=new Professor("wangwu",50);
Student s1=new Student("zhangsan",18, p);
System.out.println(s1);
Student s2=(Student)s1.deepClone();
s2.getProfessor().setName("maer");
s2.getProfessor().setAge(40);
System.out.println("复制后的:s1 = " + s1);
System.out.println("复制后的:s2 = " + s2);
}
}
五、模式的结构
介绍完浅克隆和深克隆,再回到原型模式上。
原型模式的UML类图如下:
在原型模式结构图中包含如下几个角色:
- Prototype(抽象原型类):它是声明克隆方法的接口,是所有具体原型类的公共父类,可以是抽象类也可以是接口,甚至还可以是具体实现类。
- ConcretePrototype(具体原型类):它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
- Client(客户类):让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类Prototype编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。
六、典型代码
public interface Prototype {
Prototype clone();
void setAttr(String attr);
}
public class ConcretePrototype implements Prototype {
private String attr; //成员属性
public void setAttr(String attr) {
this.attr = attr;
}
public String getAttr() {
return this.attr;
}
public Prototype clone() {
Prototype prototype = new ConcretePrototype(); //创建新对象
prototype.setAttr(this.attr);
return prototype;
/*Object object = null;
try {
object = super.clone();
} catch (CloneNotSupportedException exception) {
System.err.println("Not support cloneable");
}
return (Prototype) object;*/
}
}
clone()方法是不能直接返回this的,相信都明白。
七、代码示例
看过仙侠小说的都知道,修炼到高境界(肯定不是练气,金丹的渣渣了),就可以炼制身外化身,这些化身都有自己的思想,可以修炼自己的功法,但却听从主体的命令,当然有时候化身也会叛变,这里就以仙人化身为例:
public class Immortal implements Serializable {
private String name;
private int age;
private String magicalPower;//神通
private Wife wife;//道侣
public Immortal(String name, int age, String magicalPower) {
try {
Thread.sleep(4000);//模拟仙人修炼
} catch (InterruptedException e) {
e.printStackTrace();
}
this.name = name;
this.age = age;
this.magicalPower = magicalPower;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void setMagicalPower(String magicalPower) {
this.magicalPower = magicalPower;
}
public void setWife(Wife wife) {
this.wife = wife;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String getMagicalPower() {
return magicalPower;
}
public Wife getWife() {
return wife;
}
//使用序列化实现深克隆
public Immortal deepClone() throws IOException, ClassNotFoundException {
//将对象写入流中
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
//将对象从流中取出
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (Immortal) ois.readObject();
}
public String toString() {
return "仙人 [姓名=" + name + ", 年龄=" + age + ", 神通="
+ magicalPower + ",道侣=" + wife.getName() + "]";
}
}
public class Wife implements Serializable{
private String name;
private int age;
public Wife(String name, int age){
this.name = name;
this.age = age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String toString(){
return "道侣 [姓名=" + name + ", 年龄=" + age + "]";
}
}
public class Client {
public static void main(String[] args) {
Wife yushiqie = new Wife("雨师妾", 3000);
Immortal immortal = new Immortal("拓拔野", 2985, "天元诀,刹那芳华");
immortal.setWife(yushiqie);
System.out.println(immortal);
try {
//故事最后拓跋陪我最爱的雨师妾归隐,但姑射仙子却没了归宿,这里假设拓跋分出一个分身
Immortal incarnation = immortal.deepClone();
Wife guyexianzi = incarnation.getWife();
System.out.println(immortal.getWife() == incarnation.getWife()); //false
guyexianzi.setName("姑射仙子");
guyexianzi.setAge(2985);
System.out.println(incarnation);
incarnation.setWife(guyexianzi);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
八、优点和缺点
8.1、优点
原型模式的主要优点如下:
- 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。
- 扩展性较好,由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统都没有任何影响。
- 原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构,而原型模式就不需要这样,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。
- 可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(如恢复到某一历史状态),可辅助实现撤销操作。
8.2、缺点
原型模式的主要缺点如下:
- 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时,需要修改源代码,违背了“开闭原则”。
- 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。
九、适用环境
在以下情况下可以考虑使用原型模式:
- 创建新对象成本较大(如初始化需要占用较长的时间,占用太多的CPU资源或网络资源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改。
- 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用,即保护性拷贝。
- 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。