java设计模式-原型模式(Prototype)
定义
原型模式属于对象的创建模式。通过给出一个原型对象来指明所有创建的对象的类型,然后用复制这个原型对象的办法创建出更多同类型的对象。这就是原型模式的用意
原型模式的结构
原型模式要求对象实现同一个可以“克隆”自身的接口,遮掩个就可以通过赋值一个实例对象本身来创建一个新的实例。
这样一来,通过原型实例创建新的对象,就不再需要关心这个实例本身的类型,只要实现了克隆自身的方法,就可以通过这个方法获取新的对象,而无需再去通过new
来创建。
原型对象有两种表现形式:
- 简单形式
- 登记形式
这两种形式仅仅是原型模式的不同实现。
简单形式的原型模式
原型模式原型模式涉及三个角色:
- 客户(Client)角色:客户类提出创建对象的请求。
- 抽象原型(Prototype)角色:这是一个抽象角色,通常由一个Java接口或者Java抽象类实现。此角色给出所有的具体原型类所需的接口。
- 具体原型(Concrete Prototype)角色:被复制的对象。此角色需要实现抽象原型角色要求的接口。
示例代码
抽象原型角色
/**
* 抽象原型角色
*/
public abstract class Prototype {
private String id;
public Prototype(String id) {
this.id = id;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
/**
* 克隆自身的方法
* @return 一个从自身克隆出来的对象。
*/
public abstract Prototype clone();
}
具体原型角色
public class ConcreteProtype1 extends Prototype {
public ConcreteProtype1(String id) {
super(id);
}
public Prototype clone() {
Prototype prototype = new ConcreteProtype1(this.getId());
return prototype;
}
}
public class ConcreteProtype2 extends Prototype {
public ConcreteProtype2(String id) {
super(id);
}
public Prototype clone() {
Prototype prototype = new ConcreteProtype2(this.getId());
return prototype;
}
}
客户端
public class Client {
public static void main(String[] args) {
ConcreteProtype1 protype1 = new ConcreteProtype1("Protype1");
ConcreteProtype1 protypeCopy1 = (ConcreteProtype1)protype1.clone();
System.out.println(protypeCopy1.getId());
System.err.println(protype1.toString());
System.err.println(protypeCopy1.toString());
ConcreteProtype2 protype2 = new ConcreteProtype2("Protype2");
ConcreteProtype2 protypeCopy2 = (ConcreteProtype2)protype2.clone();
System.out.println(protypeCopy2.getId());
System.err.println(protype2.toString());
System.err.println(protypeCopy2.toString());
}
}
输出结果:
Protype1
com.sschen.prototype.ConcreteProtype1@2a139a55
com.sschen.prototype.ConcreteProtype1@15db9742
Protype2
com.sschen.prototype.ConcreteProtype2@6d06d69c
com.sschen.prototype.ConcreteProtype2@7852e922
还有另外一种对象的复制方式,如下:
ConcreteProtype1 protype3 = new ConcreteProtype1("Protype3");
ConcreteProtype1 protypeCopy3 = protype3;
System.out.println(protypeCopy3.getId());
System.err.println(protype3.toString());
System.err.println(protypeCopy3.toString());
这种方式的输出结果为:
Protype3
com.sschen.prototype.ConcreteProtype1@2a139a55
com.sschen.prototype.ConcreteProtype1@2a139a55
这种方式同上面的原型模式的克隆模式比较,可以看见的是:原型模式生成的两个对象,内存地址是不一样的。
之前在java面试 内存中堆和栈的区别文章中说明过:对象的值存放在堆中,在栈中存储指向堆中内存位置的引用。
第一种方式中,我们是重新创建了新的对象,对象的值和引用都是新的,对于克隆对象的任何变更,都不会影响到原对象的值。如下图:
原型模式存储结构图另一种方式,我们只是在栈中新创建了一个引用,指向的还是被复制对象对应的堆地址,对于复制对象的变更,都会同时改变原对象的值。如下图:
不正确的对象复制方式登记形式的原型模型
登记形式的原型模型作为原型模式的第二种形式,它多了一个原型管理器(PrototypeManager)角色。该角色的作用是:创建具体有原型类的对象,并记录每一个被创建的对象。
示例代码
抽象原型角色
public interface Prototype {
public Prototype clone();
public String getName();
public void setName(String name);
}
具体原型角色
public class ConcretePrototype1 implements Prototype {
private String name;
@Override
public Prototype clone() {
ConcretePrototype1 prototype1 = new ConcretePrototype1();
prototype1.setName(this.name);
return prototype1;
}
@Override
public String getName() {
return this.name;
}
@Override
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "ConcretePrototype1 [name=" + name + "]";
}
}
public class ConcretePrototype2 implements Prototype {
private String name;
@Override
public Prototype clone() {
ConcretePrototype2 prototype2 = new ConcretePrototype2();
prototype2.setName(this.name);
return prototype2;
}
@Override
public String getName() {
return this.name;
}
@Override
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "ConcretePrototype2 [name=" + name + "]";
}
}
原型管理器角色保持一个聚集,作为对所有原型对象的登记,这个角色提供必要的方法,供外界增加新的原型对象和取得已经登记过的原型对象。
public class PrototypeManager {
/**
* 用来记录原型的编号同原型实例的对象关系
*/
private static Map<String, Prototype> map = new HashMap<String, Prototype>();
/**
* 私有化构造方法,避免从外部创建实例
*/
private PrototypeManager() {}
/**
* 向原型管理器里面添加或者修改原型实例
* @param prototypeId 原型编号
* @param prototype 原型实例
*/
public synchronized static void setPrototype(String prototypeId, Prototype prototype) {
map.put(prototypeId, prototype);
}
/**
* 根据原型编号从原型管理器里面移除原型实例
* @param prototypeId 原型编号
*/
public synchronized static void removePrototype(String prototypeId) {
map.remove(prototypeId);
}
/**
* 根据原型编号获取原型实例
* @param prototypeId 原型编号
* @return 原型实例对象
* @throws Exception 如果根据原型编号无法获取对应实例,则提示异常“您希望获取的原型还没有注册或已被销毁”
*/
public synchronized static Prototype getPrototype(String prototypeId) throws Exception {
Prototype prototype = map.get(prototypeId);
if (prototype == null) {
throw new Exception("您希望获取的原型还没有注册或已被销毁");
}
return prototype;
}
}
客户端角色
public class Client {
public static void main(String[] args) {
try {
Prototype p1 = new ConcretePrototype1();
PrototypeManager.setPrototype("p1", p1);
//获取原型来创建对象
Prototype p3 = PrototypeManager.getPrototype("p1").clone();
p3.setName("张三");
System.out.println("第一个实例:" + p3);
//有人动态的切换了实现
Prototype p2 = new ConcretePrototype2();
PrototypeManager.setPrototype("p1", p2);
//重新获取原型来创建对象
Prototype p4 = PrototypeManager.getPrototype("p1").clone();
p4.setName("李四");
System.out.println("第二个实例:" + p4);
//有人注销了这个原型
PrototypeManager.removePrototype("p1");
//再次获取原型来创建对象
Prototype p5 = PrototypeManager.getPrototype("p1").clone();
p5.setName("王五");
System.out.println("第三个实例:" + p5);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出结果为:
第一个实例:ConcretePrototype1 [name=张三]
第二个实例:ConcretePrototype2 [name=李四]
java.lang.Exception: 您希望获取的原型还没有注册或已被销毁
at com.sschen.prototyperegist.PrototypeManager.getPrototype(PrototypeManager.java:44)
at com.sschen.prototyperegist.Client.main(Client.java:26)
两种形式的比较
简单形式和登记形式的原型模式各有其长处和短处。
- 如果需要创建的原型对象数据较少而且比较固定的话,可以采用第一种形式。在这种情况下,原型对象的引用可以由客户端自己保存。
- 如果要创建的原型对象数据不固定的话,可以采用第二种形式。在这种情况下,客户端不保存对原型对象的引用,这个任务被交给原型管理器角色。在克隆一个对象之前,客户端可以查看管理员对象是否已经有一个满足要求的原型对象。如果有,可以从原型管理器角色中取得这个对象引用;如果没有,客户端就需要自行复制此原型对象。
Java中的克隆方法
Java中的所有类都是从java.lang.Object
类继承而来的,而Object
类提供protected Object clone()
方法对对象进行克隆复制,子类当然也可以把这个方法置换掉,提供满足自己需要的复制方法。对象的复制有一个基本问题,就是对象通常都有对其他对象的引用。当使用Object
类的clone()
方法来复制一个对象时,此对象对其他对象的引用也同时会被复制一份。
java语言提供的Cloneable
接口只起一个作用,就是在运行时期通知Java虚拟机可以安全的在这个类上使用clone()
方法。通过调用这个clone()
方法可以得到一个对象的复制。由于Object
类本身不实现Cloneable
接口,因此如果所考虑的类没有实现Cloneable
接口时,调用clone()
方法会抛出CloneNotSupportedException
异常。
克隆满足的条件
clone()
方法将对象复制了一份并返还给了调用者。所谓“复制”的含义于clone()
方法是怎么实现的含义是一样的。一般而言,clone()
方法满足以下的描述:
- 对任何的对象
x
,都有x.clone() != x
。换而言之,克隆对象和原对象不是同一个对象。 - 对任何的对象
x
,都有x.clone().getClass() == x.getClass()
。换而言之,克隆对象同原对象的类型一致。 - 如果对象
x
的equals()
方法定义其恰当的话,那么x.clone().equals(x)
应当是成立的。
在Java语言的API中,凡是提供了clone()
方法的类,都满足上面的这些条件。Java语言的设计师再设计自己的clone()
方法时,也应当遵守这三个条件。一般来说,上面的三个条件中的前两个是必需的,而第三个是可选的。
浅克隆和深克隆
无论你是自己实现克隆方法,还是采用Java提供的克隆方法,都存在一个浅度克隆和深度克隆的问题。
-
浅度克隆:只负责克隆按值传递的数据(比如基本数据类型,
String
类型),而不是复制它所引用的对象。换而言之,所有对其他对象的引用都仍然指向原来的对象。 -
深度克隆:除了浅度克隆需要克隆的值外,还负责克隆引用类型的数据。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换而言之,深度克隆要把复制的对象所引用的对象都复制一遍,而这种对被引用到的对象的复制叫做简间接复制。
深度克隆要深入到多少层,是一个不易确定的问题。在决定以深度克隆的方式复制一个对象的时候,必须决定对间接复制的对象是采取浅度克隆还是继续采用深度克隆。因此,在采用深度克隆时,需要决定多深才算深。
此外,在深度克隆的过程中,很可能会出现循环引用的问题,必须小心处理。
使用序列化实现深度克隆
把对象写到流里的过程是序列化Serialization
的过程;而把对象从流中读出来的过程叫反序列化Deserialization
过程。应当指出的是,写到流里的是对象的一个拷贝,原对象仍然存在于JVM中。
在Java语言里深度克隆一个对象,常常可以先使对象实现Serializable
接口,然后把对象(实际上对象的拷贝)写到一个流里(序列化过程),再从流里读出来(反序列化过程),便可以重建对象。
public Object 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 ois.readObject();
}
这样做的前提就是对象及对象内部所有引用到的对象都是可序列化的,否则,就需要仔细考察那些不可序列化的对象是否可以设置成transient
,从而将至排除在复制过程之外。
浅度克隆显然比深度克隆更容易实现,因为Java语言的所有类都会继承一个clone()
方法,而这个clone()
方法所做的正是浅度克隆。
有一些对象,比如线程Thread
对象或者Socket
对象,是不能简单复制或者共享的。不管是使用浅度克隆还是使用深度克隆,只要涉及这样的间接对象,就必须把简介对象射程transient
而不予复制;或者由程序自行创建出相当的同等对象,权且当做复制件使用。
孙大圣的身外身法术
孙大圣的身外身本领如果在Java语言里使用原型模式来实现的话,会怎么样呢?
首先,齐天大圣The Greatest Sage
,即TheGreatestSage类扮演客户角色。齐天大圣持有一个猢狲Monkey
的实例,而猢狲就是大圣本尊。Monkey
类具有继承自java.lang.Object
的clone()
方法,因此,可以通过调用这个克隆方法来复制一个Monkey
实例。
示例代码
大圣持有金箍棒的实例,因此这里定义了一个金箍棒的类GoldRingedStaff
/**
* 金箍棒对象
*/
public class GoldRingedStaff {
/**
* 高度
*/
private float height = 100.00f;
/**
* 半径
*/
private float radius = 10.00f;
/**
* 金箍棒变大方法
*/
public void grow() {
this.radius *= 2;
this.height *= 2;
}
/**
* 金箍棒缩小方法
*/
public void shrink() {
this.radius /= 2;
this.height /= 2;
}
}
大圣本尊使用Monkey
类来表示,这个类来扮演具体的原型角色:
/**
* 猕猴类,大圣本尊由猕猴类来表示
*/
public class Monkey implements Cloneable {
/**
* 身高
*/
private int height;
/**
* 体重
*/
private int weight;
/**
* 出生日期
*/
private Date birthDay;
/**
* 金箍棒
*/
private GoldRingedStaff staff;
/**
* 构造函数,指定创建事件和给定金箍棒
*/
public Monkey() {
this.birthDay = new Date();
this.staff = new GoldRingedStaff();
}
/**
* 克隆方法,直接调用接口的克隆方法
*/
public Object clone() {
Monkey temp = null;
try {
temp = (Monkey)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
} finally {
return temp;
}
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
public Date getBirthDay() {
return birthDay;
}
public void setBirthDay(Date birthDay) {
this.birthDay = birthDay;
}
public GoldRingedStaff getStaff() {
return staff;
}
public void setStaff(GoldRingedStaff staff) {
this.staff = staff;
}
}
孙大圣本尊则使用类TheGreatestSage
来表示:
public class TheGreatestSage {
private Monkey monkey = new Monkey();
public void change() {
Monkey copyMonkey = (Monkey) monkey.clone();
System.out.println("大圣本尊的生日是:" + monkey.getBirthDay());
System.out.println("克隆大圣的生日是:" + copyMonkey.getBirthDay());
System.out.println("大圣本尊同克隆大圣是否为同一个对象:" + (monkey == copyMonkey));
System.out.println("大圣本尊持有的金箍棒 同 克隆大圣持有的金箍棒是否为同一个对象:" + (monkey.getStaff() == copyMonkey.getStaff()));
}
public static void main(String[] args) {
TheGreatestSage sage = new TheGreatestSage();
sage.change();
}
}
当运行TheGreatestSage
类时,首先创建大圣本尊对象,然后浅度克隆大圣本尊对象。程序在运行时输出的信息如下:
大圣本尊的生日是:Wed Jun 28 10:19:53 CST 2017
克隆大圣的生日是:Wed Jun 28 10:19:53 CST 2017
大圣本尊同克隆大圣是否为同一个对象:false
大圣本尊持有的金箍棒 同 克隆大圣持有的金箍棒是否为同一个对象:true
可以看出,首先,复制的大圣本尊具有和原始的大圣本尊一样的birthDay
,而本尊对象不相等,这表明他们二者是克隆关系;其次,复制的大圣本尊持有的金箍棒和原始大圣持有的金箍棒为同一个对象,这表明二者所持有的金箍棒为同一根,而非两根。
正如前面所述,继承自java.lang.Object
类的clone()
方法是浅度克隆。换而言之,齐天大圣所有化身所持有的金箍棒引用全都是指向一个对象的,这与《西游记》中描写并不一致。要想要纠正这一点,就需要考虑使用深度克隆
想要做到深度克隆,就要求所有需要复制的对象都实现java.io.Serializable
接口。
实例代码,修改为深度克隆后
孙大圣的源代码
public class TheGreatestSage {
private Monkey monkey = new Monkey();
public void change() {
Monkey copyMonkey = null;
try {
copyMonkey = (Monkey) monkey.deepClone();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("大圣本尊的生日是:" + monkey.getBirthDay());
System.out.println("克隆大圣的生日是:" + copyMonkey.getBirthDay());
System.out.println("大圣本尊同克隆大圣是否为同一个对象:" + (monkey == copyMonkey));
System.out.println("大圣本尊持有的金箍棒 同 克隆大圣持有的金箍棒是否为同一个对象:" + (monkey.getStaff() == copyMonkey.getStaff()));
}
public static void main(String[] args) {
TheGreatestSage sage = new TheGreatestSage();
sage.change();
}
}
在大圣本尊Monkey
类中,原有一个克隆方法clone()
,为浅度克隆,因此新增一个方法deepClone()
,为深度克隆。在deepClone()
方法中,大圣本尊对象(一个拷贝)被序列化,然后又被反序列化。反序列化后的对象也就成了一个深度克隆后的对象。deepClone()
方法如下:
/**
* 猕猴类,大圣本尊由猕猴类来表示
*/
public class Monkey implements Cloneable,Serializable {
//………………
/**
* 克隆方法,直接调用接口的克隆方法
*/
public Object clone() {
Monkey temp = null;
try {
temp = (Monkey)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
} finally {
return temp;
}
}
public Object deepClone() throws IOException, ClassNotFoundException {
//将对象写入流中
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(this);
//将对象从流中读取回来
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
return objectInputStream.readObject();
}
//……………………
}
对于金箍棒GoldRingedStaff
类,也让其实现java.io.Serializable
接口:
public class GoldRingedStaff implements Serializable {
//………………
}
修改后的代码运行结果为:
大圣本尊的生日是:Wed Jun 28 11:31:01 CST 2017
克隆大圣的生日是:Wed Jun 28 11:31:01 CST 2017
大圣本尊同克隆大圣是否为同一个对象:false
大圣本尊持有的金箍棒 同 克隆大圣持有的金箍棒是否为同一个对象:false
从运行结果可以看出,大圣的金箍棒同克隆大圣的金箍棒是不同的对象。这是因为使用了深度克隆,从而将大圣本尊所引用的对象也都复制了一遍,其中也包括金箍棒。
原型模式优缺点总结
原型模式的优点
原型模式允许在运行时动态的改变具体的实现类型。原型模式可以在运行期间,有客户来注册符合原型接口的实现类型,也可以动态的改变具体的实现类型,看起来接口没有任何变化,但是其实运行的已经是另外一个类实体了。因为克隆一个原型对象就类似于实例化一个类。
原型模式的缺点
原型模式最主要的缺点是每一个类都必须要配备一个克隆方法。配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类来说并不是很难,但是对于已有的类来说并不容易,可别是当一个类引用不支持序列化的间接对象,或者引用含有循环结构的时候。