原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型对象创建新的对象。
原型模式其实就是从一个对象创建另外一个可复制的对象,而且不需要知道任何创建的细节。(最常用的就是基于流的深复制)
原始模型模式
Java语言本身支持原始原型模式。所有的JavaBean都继承自Java.lang.Object,而Object类提供一个clone方法,可以将一个JavaBean对象复制一份。但是这个JavaBean必须实现一个表示接口Cloneable,表明这个JavaBean支持复制。如果一个对象没有实现这个接口而调用clone()方法,Java编译器会抛出CloneNotSupportedException异常。
变量、对象以及对象的引用
对象就是类的实例。一般情况下,一个类被实例化时,此类的所有成员,包括变量和方法,都被复制到属于此数据类型的一个新的实例中取。比如:
User user = new User();
上面的语句做了如下的事情:
(1)创建了一个User类型的变量,称为user
(2)建立了一个User类的对象
(3)使变量user指到这个对象上。
可以将上面分成下面两步:
User user; user = new User();
第一行建立了一个变量叫做user,可以指到User类对象上。但在上面第一行结束时并没有指到它上面,只是在第二行才真正指到这样的一个对象上。
第二行创建了一个对象。当对象刚被创建时是一个无名对象,随后这个对象才有了一个名字user。
也有可能一个对象被创建之后就没有名字,永远保持无名状态。
User user = new User(); user.setBirthDay(new Date());
如上面,我们知道参数的传递是通过引用传递,所以只是将地址传下去。接受参数的setBirthDay 方法也不关心它接收的对象有没有名字。
因此,对象的创建与它们的引用是独立的
克隆满足的条件
clone()方法将对象复制了一份并返还给调用者。所谓“复制”的含义与clone()方法是怎么实现的。一般而言,clone()方法满足以下的描述:
(1)对任何的对象x,都有:x.clone()!=x。换言之,克隆对象与原对象不是同一个对象。
(2)对任何的对象x,都有:x.clone().getClass() == x.getClass(),换言之,克隆对象与原对象的类型一样。
(3)如果对象x的equals()方法定义其恰当的话,那么x.clone().equals(x)应当成立的。
在JAVA语言的API中,凡是提供了clone()方法的类,都满足上面的这些条件。JAVA语言的设计师在设计自己的clone()方法时,也应当遵守着三个条件。一般来说,上面的三个条件中的前两个是必需的,而第三个是可选的。
原始模式结构:
原型模式有两种表现形式:(1)简单形式;(2)登记形式。
(1)简单形式:
三个角色:
(1)客户(Client)角色:客户类提出创建对象的请求。
(2)抽象原型(Prototype)角色:这是一个抽象角色,通常由一个Java接口或Java抽象类实现。此角色给出所有的具体原型类所需的接口。
(3)具体原型(Concrete Prototype)角色:被复制的对象。此角色需要实现抽象的原型角色所要求的接口。
如下:
package cn.qlq.prototype; public interface Prototype { Prototype clone(); }
package cn.qlq.prototype; public class ConcretePrototype1 implements Prototype { public Prototype clone() { // 没有属性就不再复制值了 Prototype prototype = new ConcretePrototype1(); return prototype; } }
package cn.qlq.prototype; public class ConcretePrototype2 implements Prototype { public Prototype clone() { // 没有属性就不再复制值了 Prototype prototype = new ConcretePrototype2(); return prototype; } }
客户端代码:
package cn.qlq.prototype; public class Client { private Prototype prototype; public Client(Prototype prototype) { this.prototype = prototype; } public void operation(Prototype example) { Prototype copyPrototype = prototype.clone(); } }
(2)登记形式
作为原型模式的第二种形式,它比简单形式多了一个原型管理器(PrototypeManager)角色,该角色的作用是:创建具体原型类的对象,并记录每一个被创建的对象。
原型管理器角色保持一个集合,作为对所有原型对象的登记,这个角色提供必要的方法,供外界增加新的原型对象和取得已经登记过的原型对象。
package cn.qlq.prototype; import java.util.HashMap; import java.util.Map; public class PrototypeManager { private static Map<String, Prototype> prototypes = new HashMap<String, Prototype>(); private PrototypeManager() { } public synchronized static void setPrototype(String prototypeId, Prototype prototype) { prototypes.put(prototypeId, prototype); } public synchronized static void removePrototype(String prototypeId) { prototypes.remove(prototypeId); } public synchronized static Prototype getPrototype(String prototypeId) throws Exception { Prototype prototype = prototypes.get(prototypeId); if (prototype == null) { throw new Exception("不存在"); } return prototype; } }
测试代码:
package cn.qlq.prototype; public class Client { public static void main(String[] args) throws Exception { Prototype p1 = new ConcretePrototype1(); PrototypeManager.setPrototype("p1", p1); // 获取原型来创建对象并且复制一份 Prototype p3 = PrototypeManager.getPrototype("p1").clone(); System.out.println(p1 == p3); } }
两种形式的比较:
如果需要创建的原型对象数目较少而且固定的话,可以采取第一种形式也就是简单形式的原始模型模式。在这种情况下,原型对象的引用由客户端自己保存。
如果要创建的原型对象数目不固定的话,采用登记形式。保存原对象的引用由管理器角色执行。在复制一个原型对象之前,客户端可以查看管理员对象是否已经有一个满足要求的的原型对象,如果有直接返回,如果没有客户端就需要自行复制此原型对象。
浅复制和深复制:
浅复制
只负责克隆按值传递的数据(比如基本数据类型、String类型),而不复制它所引用的对象,换言之,所有的对其他对象的引用都仍然指向原来的对象。
深复制
除了浅度克隆要克隆的值外,还负责克隆引用类型的数据。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深度克隆把要复制的对象所引用的对象都复制了一遍,而这种对被引用到的对象的复制叫做间接复制。
深度克隆要深入到多少层,是一个不易确定的问题。在决定以深度克隆的方式复制一个对象的时候,必须决定对间接复制的对象时采取浅度克隆还是继续采用深度克隆。因此,在采取深度克隆时,需要决定多深才算深。此外,在深度克隆的过程中,很可能会出现循环引用的问题,必须小心处理。
利用串行化实现深度克隆:(重要)
把对象写到流里的过程称为串行化,在Java中又称为"冷冻"或者"腌咸菜";而把对象从流里读出来的并行化过程则叫做"解冻"或者"回鲜"过程。应当指出的是,写到流里的是对象的一个拷贝,而原对象仍然存在于JVM里面,因此"腌咸菜"只是对象的一个拷贝。
在Java语言里深度克隆一个对象,常常可以先使对象实现Serializable接口,然后把对象(实际上只是对象的拷贝)写到一个流里(序列化),再从流里读回来(反序列化),便可以重建对象。
如下代码:
public static <T> T cloneObj(T obj) { T retVal = null; try { // 将对象写入流中 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(obj); // 从流中读出对象 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); retVal = (T) ois.readObject(); } catch (Exception e) { e.printStackTrace(); } return retVal; }
这样做的前提就是对象以及对象内部所有引用到的对象都是可序列化的,否则,就需要仔细考察那些不可序列化的对象可否设成transient,从而将之排除在复制过程之外。
浅复制显然比深复制更容易实现,因为Java语言的所有类都会继承一个clone()方法,而这个clone()方法所做的就是浅复制。
有一些对象,比如线程(Thread)对象或Socket对象,是不能简单复制或共享的。不管是使用浅复制还是深复制,只要涉及这样的间接对象,就必须把间接对象设成transient而不予复制;或者由程序自行创建出相当的同种对象,权且当做复制件使用。
如下测试代码:
User是根对象,其内部引用类也要实现Serializable接口,user的name属性用transient关键字修饰不会被序列化。
package cn.qlq.prototype; import java.io.Serializable; public class User implements Serializable { private Address address; private int id; private transient String name; private String sex; private String job; private String health; private String BMI; private int height; private int weight; public User() { super(); } public User(Address address, int id, String name, String sex, String job, String health, String bMI, int height, int weight) { super(); this.address = address; this.id = id; this.name = name; this.sex = sex; this.job = job; this.health = health; BMI = bMI; this.height = height; this.weight = weight; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } public String getJob() { return job; } public void setJob(String job) { this.job = job; } public String getHealth() { return health; } public void setHealth(String health) { this.health = health; } public String getBMI() { return BMI; } public void setBMI(String bMI) { BMI = bMI; } 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 Address getAddress() { return address; } public void setAddress(Address address) { this.address = address; } @Override public String toString() { return "User [address=" + address + ", id=" + id + ", name=" + name + ", sex=" + sex + ", job=" + job + ", health=" + health + ", BMI=" + BMI + ", height=" + height + ", weight=" + weight + "]"; } } class Address implements Serializable { private String province; private String country; public Address(String province, String country) { super(); this.province = province; this.country = country; } public String getProvince() { return province; } public void setProvince(String province) { this.province = province; } public String getCountry() { return country; } public void setCountry(String country) { this.country = country; } @Override public String toString() { return "Address [province=" + province + ", country=" + country + "]"; } }
public static void main(String[] args) throws Exception { User user = new User(); user.setName("张三"); user.setBMI("BMI"); user.setHealth("健康"); user.setHeight(50); user.setId(1); Address address = new Address("山西", "cn"); user.setAddress(address); // 克隆对象并修改属性后打印 User cloneedUser = cloneObj(user); cloneedUser.getAddress().setCountry("北京"); System.out.println(cloneedUser); System.out.println("========"); System.out.println(user); }
结果:(name属性没有被复制上,而且其address是深复制)
User [address=Address [province=山西, country=北京], id=1, name=null, sex=null, job=null, health=健康, BMI=BMI, height=50, weight=0]
========
User [address=Address [province=山西, country=cn], id=1, name=张三, sex=null, job=null, health=健康, BMI=BMI, height=50, weight=0]
原型模式的优点
原型模式允许在运行时动态改变具体的实现类型。原型模式可以在运行期间,由客户来注册符合原型接口的实现类型,也可以动态地改变具体的实现类型,看起来接口没有任何变化,但其实运行的已经是另外一个类实例了。因为克隆一个原型就类似于实例化一个类。
原型模式的缺点
原型模式最主要的缺点是每一个类都必须配备一个克隆方法。配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类来说不是很难,而对于已经有的类不一定很容易,特别是当一个类引用不支持序列化的间接对象,或者引用含有循环结构的时候。
适用场景:
假设一个系统的产品类是动态加载的,而且产品具有一定的等级结构。这个时候如果使用工厂模式的话,工厂模式就得具有一定的结构。而产品类的等级结构发生变化,工厂类的等级结构就得随之变化。这对于产品结构经常可能会变的系统来说,采用工厂模式就不方便。
这时候就可以采用原型模式,给每个产品装配一个克隆方法(大多数时候只需给产品类的根类装配克隆方法)。
总结:
这种模式使用的最多的就是深复制,项目中也没用到这种模式。