Java设计模式-原型模式(Prototype Pattern)
目录
- 什么是原型模式
- 原型模式的2种实现方式
- JavaSE原型模式的应用
- Struts2原型模式的应用
原型模式属于对象的创建模式。通过给出一个原型对象来指明所有创建的对象的类型,然后用复制这个原型对象的办法创建出更多同类型的对象。这就是选型模式的用意。
一、什么是原型模式
用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。原型实例指定了要创建对象的种类,创建时无需知道对象创建细节。
原型模式要求对象实现一个可以“克隆”自身的接口,这样就可以通过复制一个实例对象本身来创建一个新的实例。这样一来,通过原型实例创建新的对象,就不再需要关心这个实例本身的类型,只要实现了克隆自身的方法,就可以通过这个方法来获取新的对象,而无须再去通过new来创建。
这种形式涉及到三个角色:
- 客户(Client)角色:客户类提出创建对象的请求。
- 抽象原型(Prototype)角色:抽象角色,通常由一个Java接口或Java抽象类实现,此角色给出所有的具体原型类所需的接口(如Object的clone方法)。
- 具体原型(Concrete Prototype)角色:被复制的对象,实现复制接口
二、原型模式的2种实现方式
使用原型模式,完成克隆羊多利
2.1 自定义克隆方法
package org.PrototypePattern;
interface Sheep{
public DuoLiSheep clone();
}
class DuoLiSheep implements Sheep{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public DuoLiSheep clone() {
DuoLiSheep duoLiSheep = new DuoLiSheep();
duoLiSheep.setName(this.getName());
return duoLiSheep;
}
}
public class Custom {
public static void main(String[] args) {
// 创建一个对象实例
DuoLiSheep duoLiSheep = new DuoLiSheep();
duoLiSheep.setName("多利");
// 非克隆模式克隆对象
DuoLiSheep duoLiSheep1 = new DuoLiSheep();
// 如果对象内部很复杂,则需要非常多的构建细节,容易出错且繁琐
duoLiSheep1.setName(duoLiSheep.getName());
// 自定义原型模式克隆对象,无需关注如何实现,只要调用clone我就能获取一个新的对象
DuoLiSheep duoLiSheep2 = duoLiSheep.clone();
System.out.println("多利1号与多利2号是同一个对象吗? " + (duoLiSheep.equals(duoLiSheep2) ?"是":"不是"));
}
}
// 运行结果
多利1号与多利2号是同一个对象吗? 不是
上面的代码中写了普通克隆方案和原型模式的克隆方法,可以总结出
- 原型模式操作简单,屏蔽对象创建细节
- 普通的克隆方法需要获取原始对象的属性,一一赋值给新对象,过于复杂
2.1 Object类的clone方法
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()方法时,也应当遵守着三个条件。一般来说,上面的三个条件中的前两个是必需的,而第三个是可选的。
接下来代码实现克隆羊案例
package org.PrototypePattern.version2;
class DuoLiSheep implements Cloneable{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public Object clone() throws CloneNotSupportedException {
System.out.println("多利羊复制成功!");
return (DuoLiSheep) super.clone();
}
}
public class JDKClone {
public static void main(String[] args) throws CloneNotSupportedException {
DuoLiSheep duoLiSheep = new DuoLiSheep();
duoLiSheep.setName("多利");
DuoLiSheep duoLiSheep1 = (DuoLiSheep) duoLiSheep.clone();
System.out.println("两个对象是否相同?" + (duoLiSheep1.equals(duoLiSheep)));
}
}
// 运行结果如下
多利羊复制成功!
两个对象是否相同?false
通过实现Cloneable接口的方法优缺点如下:
优点
- Java 自带的原型模式基于内存二进制流的复制,在性能上比直接 new 一个对象更加优良。
- 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份,并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。
缺点
- 需要为每一个类都配置一个clone方法
- clone方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违背了开闭原则。
- 当实现深克隆时,需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦。因此,深克隆、浅克隆需要运用得当。
浅拷贝是什么?
数据类型是基本数据类型或String,浅拷贝直接进行值传递,即将属性值直接复制一份给新的对象。
数据类型是引用类型(对象、数组),浅拷贝会进行引用传递,也就是将成员变量的引用值(内存地址)复制一份给新的对象。如属性A是一个引用类型,那么浅拷贝后newObject.A=oldObject.A。如果A有修改,将影响到newObject和oldObject。
上面用原型模式改进的克隆羊的例子就是浅拷贝,浅拷贝是用默认的clone()来实现的
深拷贝是什么?
相对于浅拷贝来说,深拷贝对基本类型或String类型的处理是一样的,不同的是处理引用类型。深拷贝会为所有的引用类型的成员变量申请存储空间,并复制每个引用数据成员变量所引用的对象,直到改对象可达的所有对象。 深拷贝的两种实现方式
方式1:重写clone方法来实现深度克隆
package org.PrototypePattern;
import java.util.ArrayList;
import java.util.List;
class DuoLiSheep implements Cloneable{
private String name;
private List child = new ArrayList();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List getChild() {
return child;
}
public void setChild(List child) {
this.child = child;
}
@Override
public Object clone() throws CloneNotSupportedException {
DuoLiSheep dl = (DuoLiSheep) super.clone();
if (dl.getChild().size() > 0){
dl.setChild(this.getChild());
}
System.out.println("多利羊复制成功!");
return dl;
}
}
public class DeepCopy {
public static void main(String[] args) throws CloneNotSupportedException {
DuoLiSheep duoLiSheep = new DuoLiSheep();
duoLiSheep.setName("多利");
DuoLiSheep duoLiSheep1 = (DuoLiSheep) duoLiSheep.clone();
System.out.println("两个对象是否相同?" + (duoLiSheep1.equals(duoLiSheep)));
}
}
// 运行结果如下
多利羊复制成功!
两个对象是否相同?false
方式2:利用序列化实现深度克隆
package org.PrototypePattern.version4;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
class DuoLiSheep implements Cloneable, Serializable {
private String name;
private List child = new ArrayList();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List getChild() {
return child;
}
public void setChild(List child) {
this.child = child;
}
@Override
public Object clone() throws CloneNotSupportedException {
//创建流对象
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
ByteArrayInputStream bis = null;
ObjectInputStream ois = null;
try {
// 序列化
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
//当前这个对象以对象流的方式输出
oos.writeObject(this);
// 反序列化,bos存放了对象流,装饰模式中我们讲过其中原理
// ObjectOutputStream是对ByteArrayOutputStream增强装饰而已,流在bos中
// 序列化是对象转byte,反序列化是将byte转对象
bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);
// 读取数据流实现反序列化
DuoLiSheep copyObj = (DuoLiSheep) ois.readObject();
return copyObj;
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
try {
bos.close();
oos.close();
bis.close();
ois.close();
} catch (Exception e2) {
e2.printStackTrace();
}
}
}
}
public class DeepCopy {
public static void main(String[] args) throws CloneNotSupportedException {
DuoLiSheep duoLiSheep = new DuoLiSheep();
duoLiSheep.setName("多利");
DuoLiSheep duoLiSheep1 = (DuoLiSheep) duoLiSheep.clone();
System.out.println("两个对象是否相同?" + (duoLiSheep1.equals(duoLiSheep)));
}
}
// 运行结果
两个对象是否相同?false
反序列化可以实现对象的深拷贝,其实就是创建了新的对象,在单例模式中我们提到过反序列化可以破坏单例模式,就是因为反序列化创建的是新的对象,而且属性与旧对象完全相同,无需区别对象的属性是简单类型还是负责类型。
原型模式的注意事项和细节
- 创建新的对象比较复杂时,可以利用原型模式简化对象的创建过程,同时也能够提高效率
- 不用重新初始化对象,而是动态地获得对象运行时的状态
- 如果原始对象发生变化(增加或者减少属性),其它克隆对象的也会发生相应的变化,无需修改代码。
- 在实现深克隆的时候可能需要比较复杂的代码
- 缺点:需要为每一个类配备一个克隆方法,这对全新的类来说不是很难,但对已有的类进行改造时,需要修改其源代码,违背了ocp原则。
三、JavaSE原型模式的应用
暂时有类使用了该模式,如果有,请私信我,我来补充,谢谢
四、Struts2原型模式的应用
很多文章都在说:”Struts2中保证线程的安全性,Action 对象的创建使用了原型模式,访问一个已经存在Action对象时将通过克隆的方式创建出一个新的对象,从而保证其中定义的变量无须进行加锁实现同步,每个Action中都有自己的成员变量,避免Struts1因使用单利模式而导致的并发和同步问题“
但我调试了2.0.8与2.5.30,对象的创建都是通过ObjectFactory的buildBean方法构建Action对象
public Object buildBean(Class clazz, Map<String, Object> extraContext) throws Exception {
return clazz.newInstance();
}
没发现使用原型模式,如果读者发现确实使用了原型模式,还请私信我修改,谢谢。