定义

原型模式属于对象的创建模式。通过给出一个原型对象来指明所有创建的对象的类型,然后用复制这个原型对象的办法创建出更多同类型的对象。这就是原型模式的用意

原型模式的结构

原型模式要求对象实现同一个可以“克隆”自身的接口,遮掩个就可以通过赋值一个实例对象本身来创建一个新的实例。

这样一来,通过原型实例创建新的对象,就不再需要关心这个实例本身的类型,只要实现了克隆自身的方法,就可以通过这个方法获取新的对象,而无需再去通过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 map = new HashMap();

/**

* 私有化构造方法,避免从外部创建实例

*/

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

从运行结果可以看出,大圣的金箍棒同克隆大圣的金箍棒是不同的对象。这是因为使用了深度克隆,从而将大圣本尊所引用的对象也都复制了一遍,其中也包括金箍棒。

原型模式优缺点总结

原型模式的优点

原型模式允许在运行时动态的改变具体的实现类型。原型模式可以在运行期间,有客户来注册符合原型接口的实现类型,也可以动态的改变具体的实现类型,看起来接口没有任何变化,但是其实运行的已经是另外一个类实体了。因为克隆一个原型对象就类似于实例化一个类。

原型模式的缺点

原型模式最主要的缺点是每一个类都必须要配备一个克隆方法。配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类来说并不是很难,但是对于已有的类来说并不容易,可别是当一个类引用不支持序列化的间接对象,或者引用含有循环结构的时候。

参考

java prototype是什么,java设计模式-原型模式(Prototype)相关推荐

  1. C++设计模式——原型模式(Prototype Pattern)

    C++设计模式--原型模式(Prototype Pattern) 微信公众号:幼儿园的学霸 目录 文章目录 C++设计模式--原型模式(Prototype Pattern) 目录 定义 代码示例 普通 ...

  2. Java设计模式--原型模式Prototype

    原型模式Prototype 原型模式使得用户可以通过复制对象样本来创建新对象.与通过调用构造函数创建对象相比,二者主要区别在于:通过复制创建的新对象一般会包含原始对象的某些状态. 原型模式属于对象的创 ...

  3. 设计模式---原型模式(Prototype Pattern)

    在编程中有时候我们会发现,当我们需要一个实例,可是这个实例的创建过程十分复杂,在执行过程中 会消耗大量的时间,同时创建第一个实例和创建第二个时间的初始化信息并未改变.在此种情况下,直接New 一个实例 ...

  4. 设计模式-原型模式(Prototype)

    Specify the kinds of objects to create using a prototypical instance,and create new objects bycopyin ...

  5. 原型模式(ProtoType) - Java里的对象复制

    一, 引用的复制和对象复制. 在编程中, 我们有时会用两个引用指向同一个对象. 例如: ArrayList a = new ArrayLIst(); ArrayList b = a; 看起来好像有a, ...

  6. Java常用设计模式————原型模式(一)

    介绍 原型模式(Prototype Pattern):用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象. 原型模式用于创建重复的对象,同时又能保证性能.当直接创建对象的代价比较大时,则采用 ...

  7. java设计模式---原型模式

    在阎宏博士的<JAVA与模式>一书中开头是这样描述原型(Prototype)模式的: 原型模式属于对象的创建模式.通过给出一个原型对象来指明所有创建的对象的类型,然后用复制这个原型对象的办 ...

  8. 原型模式 java 深浅_Java设计模式——原型模式

    原型模式(Prototype) 原型模式虽然是创建型的模式,但是与工程模式没有关系,从名字即可看出,该模式的思想就是将一个对象作为原型,对其进行复制.克隆,产生一个和原对象类似的新对象.本小结会通过对 ...

  9. java 复印件效果_简历复印—原型模式

    <大话设计模式>书中描述原型(Prototype)模式: 原型模式(Prototype):用用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象. 原型模式(Prototype ...

  10. 设计模式(23):创建型-原型模式(Prototype)

    设计模式(Design pattern)是一套被反复使用.多数人知晓的.经过分类编目的.代码设计经验的总结.使用设计模式是为了可重用代码.让代码更容易被他人理解.保证代码可靠性. 毫无疑问,设计模式于 ...

最新文章

  1. python根据二叉树的前序遍历和中序遍结果历重建二叉树
  2. 什么是奇异值?奇异值分解是什么?SVD分解详解及实战
  3. Nuget很慢,我们该怎么办
  4. 透过散射薄膜成像方案整理
  5. android 树形目录结构的实现(包含源码)
  6. mongodb输错命令后不能删除问题
  7. TP、PHP同域不同子级域名共享Session、单点登录
  8. 中科院计算机学院研究生招生名额,中科院研究生招生
  9. c语言中scanf输入判断,无法判断某一个输入的字符?scanf()
  10. 安卓判断服务器返回的状态码,关于服务器返回的十四种常见HTTP状态码详解
  11. 分享Silverlight/WPF/Windows Phone一周学习导读(11月6日-11月12日)
  12. (转)UML类图与类的关系详解
  13. Android四大组件简介
  14. 做机器学习算法工程师是什么样的工作体验?
  15. 计算机二级c语言选择题资料,计算机二级C语言重点选择题笔试复习资料
  16. microsoftstore连不上网_修复Microsoft Store 无法连接网络 代码: 0x80072EFD
  17. 22处令人叹为观止的景观
  18. 共享单车之摩拜与ofo优缺大盘点
  19. Django项目:前后端联调/ModelViewSet
  20. C++ primer 5版第六章

热门文章

  1. Spring Data对Cassandra 3的支持
  2. Java Web应用程序的SecureLogin
  3. restful服务端客户端_测试RESTful服务的客户端
  4. javafx基础教程_JavaFX教程–基础
  5. dao层通用封装_DAO层–救援通用
  6. 状态机复合状态 怎么写代码_状态不属于代码
  7. 实用常识_实用垃圾收集,第1部分–简介
  8. 高tps、低延迟_如何在不到1ms的延迟内完成100K TPS
  9. 休眠自动冲洗的黑暗面
  10. 使用Java 8和Lambda简化ReadWriteLock