对象的克隆-原型模式
孙悟空可以用猴毛根据自己的形象,复制 (又称“克隆”或“拷贝”)出很多跟自己长得一模一样的“身外身”来。在设计模式中也存在一个 类似的模式,可以通过一个原型对象克隆出多个一模一样的对象,该模式称之为原型模式。
先来看下它的定义:
原型模式(Prototype Pattern):使用原型实例指定创建对象的种类,并且通过拷贝这些 原型创建新的对象。原型模式是一种对象创建型模式。
原型模式结构:
在原型模式结构图中包含如下几个角色:
●Prototype(抽象原型类):它是声明克隆方法的接口,是所有具体原型类的公共父类,可以 是抽象类也可以是接口,甚至还可以是具体实现类。
● ConcretePrototype(具体原型类):它实现在抽象原型类中声明的克隆方法,在克隆方法中 返回自己的一个克隆对象。
● Client(客户类):让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直 接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到 多个相同的对象。由于客户类针对抽象原型类Prototype编程,因此用户可以根据需要选择具体 原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。
原型模式的核心在于如何实现克隆方法,下面将介绍两种在Java语言中常用的克隆实现方法:
通用的克隆实现方法是在具体原型类的克隆方法中实例化一个与自身类型相同的对象并将其 返回,并将相关的参数传入新创建的对象中,保证它们的成员属性相同。示意代码如下所 示:
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; } } |
1. Java语言提供的clone()方法
学过Java语言的人都知道,所有的Java类都继承自java.lang.Object。事实上,Object类提供一个 clone()方法,可以将一个Java对象复制一份。因此在Java中可以直接使用Object提供的clone()方 法来实现对象的克隆,Java语言中的原型模式实现很简单。
需要注意的是能够实现克隆的Java类必须实现一个标识接口Cloneable,表示这个Java类支持被 复制。如果一个类没有实现这个接口但是调用了clone()方法,Java编译器将抛出一个 CloneNotSupportedException异常。如下代码所示:
class ConcretePrototype implements Cloneable { …… public Prototype clone() { Object object = null; try { object = super.clone(); } catch (CloneNotSupportedException exception) { System.err.println("Not support cloneable"); } return (Prototype )object; } …… } |
在客户端创建原型对象和克隆对象也很简单,如下代码所示:
Prototype obj1 = new ConcretePrototype(); Prototype obj2 = obj1.clone();
一般而言,Java语言中的clone()方法满足:
(1) 对任何对象x,都有x.clone() != x,即克隆对象与原型对象不是同一个对象;
(2) 对任何对象x,都有x.clone().getClass() == x.getClass(),即克隆对象与原型对象的类型一 样;
(3) 如果对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立。
为了获取对象的一份拷贝,我们可以直接利用Object类的clone()方法,具体步骤如下:
(1) 在派生类中覆盖基类的clone()方法,并声明为public;
(2) 在派生类的clone()方法中,调用super.clone();
(3)派生类需实现Cloneable接口。
此时,Object类相当于抽象原型类,所有实现了Cloneable接口的类相当于具体原型类。
举个栗子:
周报问题: ,由于某些岗位每周工作存在重复性,工作周报内容都 大同小异。。这些周报只有一些小地方存在差异,但是现行系统每周 默认创建的周报都是空白报表,用户只能通过重新输入或不断复制粘贴来填写重复的周报内 容,极大降低了工作效率,浪费宝贵的时间。如何快速创建相同或者相似的工作周报,成为公司OA开发人员面临的一个新问题。
公司的开发人员通过对问题进行仔细分析,决定按照如下思路对工作周报模块进行重新 设计和实现:
(1)除了允许用户创建新周报外,还允许用户将创建好的周报保存为模板;
(2)用户在再次创建周报时,可以创建全新的周报,还可以选择合适的模板复制生成一份相同 的周报,然后对新生成的周报根据实际情况进行修改,产生新的周报。
只要按照如上两个步骤进行处理,工作周报的创建效率将得以大大提高。
这个过程让我们想 到平时经常进行的两个电脑基本操作:复制和粘贴,快捷键通常为Ctrl + C和Ctrl + V,
通过对 已有对象的复制和粘贴,我们可以创建大量的相同对象。
如何在一个面向对象系统中实现对 象的复制和粘贴呢?原型模式正为解决此类问题而诞生。
WeeklyLog充当具体原型类,Object类充当抽象原型类,clone()方法为原型方法。
class WeeklyLog implements Cloneable { private String name; private String date; private String content; public void setName(String name) { this.name = name; } public void setDate(String date) { this.date = date; } public void setContent(String content) { this.content = content; } public String getName() { return (this.name); } public String getDate() { return (this.date); } public String getContent() { return (this.content); } //克隆方法clone(),此处使用Java语言提供的克隆机制 public WeeklyLog clone() { Object obj = null; try { obj = super.clone(); return (WeeklyLog) obj; } catch (CloneNotSupportedException e) { System.out.println("不支持复制!"); return null; } } } |
编写如下客户端测试代码:
class Client { public static void main(String args[]) { WeeklyLog log_previous = new WeeklyLog(); //创建原型对象 log_previous.setName("张无忌"); log_previous.setDate("第12周"); log_previous.setContent("这周工作很忙,每天加班!"); System.out.println("****周报****"); System.out.println("周次:" + log_previous.getDate()); System.out.println("姓名:" + log_previous.getName()); System.out.println("内容:" + log_previous.getContent()); System.out.println("--------------------------------"); WeeklyLog log_new; log_new = log_previous.clone(); //调用克隆方法创建克隆对象 log_new.setDate("第13周"); System.out.println("****周报****"); System.out.println("周次:" + log_new.getDate()); System.out.println("姓名:" + log_new.getName()); System.out.println("内容:" + log_new.getContent()); } } |
编译并运行程序,输出结果如下:
****周报****
周次:第12周
姓名:张无忌
内容:这周工作很忙,每天加班!
-------------------------------
****周报****
周次:第13周
姓名:张无忌
内容:这周工作很忙,每天加班!
问题解决!!!
浅克隆和深克隆
思考一个问题:如果使用上述原型模式来复制周报,周报虽然可以复制,但是周报的附件并不能复制,因为你复制的是同一个引用地址。?如何才能实现周报和附件的同时复制呢?
在解决这个问题之前我们先来介绍一下两种不同的克隆方法:浅克隆和深克隆。
在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括 int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类 型。浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制。
浅克隆:
在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的 成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆 对象的成员变量指向相同的内存地址。简单来说,在浅克隆中,当对象被复制时只复制它本 身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。在Java语言中,通过覆盖Object类的clone()方法可以实现浅克隆。
深克隆:
在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象, 深克隆将原型对象的所有引用对象也复制一份给克隆对象。简单来说,在深克隆中,除了对 象本身被复制外,对象所包含的所有成员变量也将复制
在Java语言中,如果需要实现深克隆,可以通过序列化(Serialization)等方式来实现。序列化就 是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存 中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此 通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能 够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。
看一下深度拷贝上面提到的周报的代码实现:
附件类:
import java.io.*; //附件类 class Attachment implements Serializable { private String name; //附件名 public void setName(String name) { this.name = name; } public String getName() { return this.name; } public void download() { System.out.println("下载附件,文件名为" + name); } } |
工作周报类WeeklyLog不再使用Java自带的克隆机制,而是通过序列化来从头实现对象的深克 隆,我们需要重新编写clone()方法,修改后的代码如下:
import java.io.*; //工作周报类 class WeeklyLog implements Serializable { private Attachment attachment; private String name; private String date; private String content; public void setAttachment(Attachment attachment) { this.attachment = attachment; } public void setName(String name) { this.name = name; } public void setDate(String date) { this.date = date; } public void setContent(String content) { this.content = content; } public Attachment getAttachment() { return (this.attachment); } public String getName() { return (this.name); } public String getDate() { return (this.date); } public String getContent() { return (this.content); } //使用序列化技术实现深克隆 public WeeklyLog deepClone() throws IOException, ClassNotFoundException { //将对象写入流中 ByteArrayOutputStream bao = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bao); oos.writeObject(this); //将对象从流中取出 ByteArrayInputStream bis = new ByteArrayInputStream(bao.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return (WeeklyLog) ois.readObject(); } } |
客户端代码如下所示:
class Client { public static void main(String args[]) { WeeklyLog log_previous, log_new = null; log_previous = new WeeklyLog(); //创建原型对象 Attachment attachment = new Attachment(); //创建附件对象 log_previous.setAttachment(attachment); //将附件添加到周报中 try { log_new = log_previous.deepClone(); //调用深克隆方法 } catch (Exception e) { System.err.println("克隆失败!"); } //比较周报 System.out.println("周报是否相同? " + (log_previous == log_new)); //比较附件 System.out.println("附件是否相同? " + (log_previous.getAttachment()==log_new.getAttachment())); } } |
编译并运行程序,输出结果如下:
周报是否相同? false 附件是否相同? false
从输出结果可以看出,由于使用了深克隆技术,附件对象也得以复制,因此用“==”比较原型 对象的附件和克隆对象的附件时输出结果均为false。深克隆技术实现了原型对象和克隆对象的 完全独立,对任意克隆对象的修改都不会给其他对象产生影响,是一种更为理想的克隆实现 方式。
原型模式总结
原型模式作为一种快速创建大量相同或相似对象的方式,在软件开发中应用较为广泛,很多 软件提供的复制(Ctrl + C)和粘贴(Ctrl + V)操作就是原型模式的典型应用,下面对该模式的使 用效果和适用情况进行简单的总结
1.主要优点
原型模式的主要优点如下:
(1) 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个 已有实例可以提高新实例的创建效率。
(2) 扩展性较好,由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编 程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统都没有任何影响。
(3) 原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的 工厂等级结构,而原型模式就不需要这样,原型模式中产品的复制是通过封装在原型类中的 克隆方法实现的,无须专门的工厂类来创建产品。
(4) 可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起 来,以便在需要的时候使用(如恢复到某一历史状态),可辅助实现撤销操作。
2.主要缺点
原型模式的主要缺点如下:
(1) 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进 行改造时,需要修改源代码,违背了“开闭原则”。
(2) 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了 实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。
3.适用场景 在以下情况下可以考虑使用原型模式:
(1) 创建新对象成本较大(如初始化需要占用较长的时间,占用太多的CPU资源或网络资 源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对 其成员变量稍作修改。
(2) 如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占用内存较少时,可 以使用原型模式配合备忘录模式来实现。
(3) 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的 几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方 便。