设计模式解密(18)- 原型模式
1、简介
定义:用原型实例指定创建对象的种类,并且通过拷贝这些原型来创建新的对象。
功能:①是通过克隆来创建新的对象实例;②是为克隆出来的新的对象实例复制原型实例属性的值。
本质:通过克隆来创建新的对象实例。
英文:Prototype
类型:创建型
2、类图及组成
(引)类图:
组成:
Prototype:声明一个克隆自身的接口,用来约束想要克隆自己的类,要求它们都要实现这里定义的克隆方法。
ConcretePrototype:实现Prototype接口的类,这些类真正实现了克隆自身的功能。
Client:使用原型的客户端,首先要获取到原型实例对象,然后通过原型实例克隆自身来创建新的对象实例。
标准代码结构:
/** * 先来看看原型接口的定义 : 声明一个克隆自身的接口 */ public interface Prototype { /** * 克隆自身的方法 * @return 一个从自身克隆出来的对象 */ public Prototype clone(); } /** * 具体的原型实现对象 : 克隆的具体实现对象 * 为了跟上面原型模式的结构示意图保持一致,因此这两个具体的原型实现对象,都没有定义属性。事实上,在实际使用原型模式的应用中,原型对象多是有属性的,克隆原型的时候也是需要克隆原型对象的属性的,特此说明一下。 */ public class ConcretePrototype implements Prototype { public Prototype clone() { //最简单的克隆,新建一个自身对象,由于没有属性,就不去复制值了 Prototype prototype = new ConcretePrototype(); return prototype; } } /** * 使用原型的客户端 */ public class Client { /** * 持有需要使用的原型接口对象 */ private Prototype prototype; /** * 构造方法,传入需要使用的原型接口对象 * @param prototype 需要使用的原型接口对象 */ public Client(Prototype prototype){ this.prototype = prototype; } /** * 示意方法,执行某个功能操作 */ public void operation(){ //会需要创建原型接口的对象 Prototype newPrototype = prototype.clone(); } }
上面的代码组成是为了让大家清楚了解原型模式的结构,Java中已经提供Cloneable接口,所以在实现原型模式不必在实现Prototype原型接口,直接实现Cloneable接口即可;
Java实现原型模式的两个要点:
1、实现Cloneable接口。在java语言有一个Cloneable接口,它的作用只有一个,就是在运行时通知虚拟机可以安全地在实现了此接口的类上使用clone方法。在java虚拟机中,只有实现了这个接口的类才可以被拷贝,否则在运行时会抛出 CloneNotSupportedException异常。
2、重写Object类中的clone方法。Java中,所有类的父类都是Object类,Object类中有一个clone方法,作用是返回对象的一个拷贝,但是其作用域protected类型的,一般的类无法调用,因此,Prototype类需要将clone方法的作用域修改为public类型。
Java原型模式代码结构:
public class PrototypeClass implements Cloneable{ //覆写父类Object方法 @Override public PrototypeClass clone(){ PrototypeClass prototypeClass = null; try { prototypeClass = (PrototypeClass)super.clone(); } catch (CloneNotSupportedException e) { //异常处理 } return prototypeClass; } }
注意:
Object类的clone方法只会拷贝java中的8中基本类型以及他们的封装类型,另外还有String类型。对于数组、容器对象、引用对象等都不会拷贝,这就是浅拷贝。如果要实现深拷贝,必须将原型模式中的数组、容器对象、引用对象等另行拷贝。
3、浅拷贝与深拷贝
▶ 浅拷贝(shallow copy)
被复制对象的所有变量都含有与原来的对象相同的值(仅对于简单的值类型数据),而所有的对其他对象的引用都仍然指向原来的对象。换言之,只负责克隆按值传递的数据(比如:基本数据类型、String类型)。
▶ 深拷贝 (deep copy)
被复制对象的所有的变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,除了浅度克隆要克隆的值外,还负责克隆引用类型的数据,基本上就是被克隆实例所有的属性的数据都会被克隆出来。
浅拷贝实例↓↓↓:
package com.designpattern.Prototype.shallowcopy; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * 计划 * @author Json<<json1990@foxmail.com>> */ public class Plan implements Cloneable { //计划名称 private String name; //任务级别 private int level; //开始时间 private Date startdate; //截止时间 private Date enddate; //执行人员 private List<String> executors = new ArrayList<String>(); @Override public Plan clone(){ try { return (Plan) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Date getStartdate() { return startdate; } public void setStartdate(Date startdate) { this.startdate = startdate; } public Date getEnddate() { return enddate; } public void setEnddate(Date enddate) { this.enddate = enddate; } public List<String> getExecutors() { return executors; } public void setExecutors(List<String> executors) { this.executors = executors; } public int getLevel() { return level; } public void setLevel(int level) { this.level = level; } @Override public String toString() { return "[name=" + name + ", level=" + level + ", startdate=" + startdate + ", enddate=" + enddate + ", executors=" + executors + "]"; } }
测试:
package com.designpattern.Prototype.shallowcopy; import java.util.ArrayList; import java.util.List; import com.designpattern.utils.DateFormatUtil; /** * 测试 * @author Json<<json1990@foxmail.com>> */ public class Client { public static void main(String[] args) { List<String> executors = new ArrayList<String>(); executors.add("张三"); executors.add("李四"); Plan plan = new Plan(); plan.setName("重构前端登录界面"); plan.setLevel(1); plan.setStartdate(DateFormatUtil.stringToDate(DateFormatUtil.YYYYMMDD,"2017-08-07")); plan.setEnddate(DateFormatUtil.stringToDate(DateFormatUtil.YYYYMMDD,"2017-08-09")); plan.setExecutors(executors); Plan plan2 = plan.clone(); plan2.setName("后端接口改造"); plan2.setLevel(2); plan2.setStartdate(DateFormatUtil.stringToDate(DateFormatUtil.YYYYMMDD,"2017-08-10")); plan2.setEnddate(DateFormatUtil.stringToDate(DateFormatUtil.YYYYMMDD,"2017-08-12")); System.out.println("地址是否一样?"+(plan == plan2)); System.out.println("plan.getName() == plan2.getName() "+(plan.getName() == plan2.getName())); System.out.println("plan.getLevel() == plan2.getLevel() "+(plan.getLevel() == plan2.getLevel())); System.out.println("plan.getStartdate() == plan2.getStartdate() "+(plan.getStartdate() == plan2.getStartdate())); System.out.println("plan.getEnddate() == plan2.getEnddate() "+(plan.getEnddate() == plan2.getEnddate())); System.out.println("plan.getExecutors() == plan2.getExecutors() "+(plan.getExecutors() == plan2.getExecutors())); System.out.println("plan:"+plan.toString()); System.out.println("plan2:"+plan2.toString()); //plan任务比较重,在给plan添加一个人 executors.add("王五"); plan.setExecutors(executors); System.out.println(); System.out.println("地址是否一样?"+(plan == plan2)); System.out.println("plan.getName() == plan2.getName() "+(plan.getName() == plan2.getName())); System.out.println("plan.getLevel() == plan2.getLevel() "+(plan.getLevel() == plan2.getLevel())); System.out.println("plan.getStartdate() == plan2.getStartdate() "+(plan.getStartdate() == plan2.getStartdate())); System.out.println("plan.getEnddate() == plan2.getEnddate() "+(plan.getEnddate() == plan2.getEnddate())); System.out.println("plan.getExecutors() == plan2.getExecutors() "+(plan.getExecutors() == plan2.getExecutors())); System.out.println("plan:"+plan.toString()); System.out.println("plan2:"+plan2.toString()); } }
结果:
地址是否一样?false
plan.getName() == plan2.getName() false
plan.getLevel() == plan2.getLevel() false
plan.getStartdate() == plan2.getStartdate() false
plan.getEnddate() == plan2.getEnddate() false
plan.getExecutors() == plan2.getExecutors() true
plan:[name=重构前端登录界面, level=1, startdate=Mon Aug 07 00:00:00 CST 2017, enddate=Wed Aug 09 00:00:00 CST 2017, executors=[张三, 李四]]
plan2:[name=后端接口改造, level=2, startdate=Thu Aug 10 00:00:00 CST 2017, enddate=Sat Aug 12 00:00:00 CST 2017, executors=[张三, 李四]]
地址是否一样?false
plan.getName() == plan2.getName() false
plan.getLevel() == plan2.getLevel() false
plan.getStartdate() == plan2.getStartdate() false
plan.getEnddate() == plan2.getEnddate() false
plan.getExecutors() == plan2.getExecutors() true
plan:[name=重构前端登录界面, level=1, startdate=Mon Aug 07 00:00:00 CST 2017, enddate=Wed Aug 09 00:00:00 CST 2017, executors=[张三, 李四, 王五]]
plan2:[name=后端接口改造, level=2, startdate=Thu Aug 10 00:00:00 CST 2017, enddate=Sat Aug 12 00:00:00 CST 2017, executors=[张三, 李四, 王五]]
通过上面的实例,大家发现没有,对于引用类型List,修改plan的List,plan2也随着变化了,这就是浅拷贝带来的问题;
深拷贝实例(修改浅拷贝实例)↓↓↓:
package com.designpattern.Prototype.deepcopy; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * 计划 * @author Json<<json1990@foxmail.com>> */ public class Plan implements Cloneable { //计划名称 private String name; //任务级别 private int level; //开始时间 private Date startdate; //截止时间 private Date enddate; //执行人员 private List<String> executors = new ArrayList<String>(); @Override public Plan clone(){ try { Plan plan = (Plan) super.clone(); //引用类型的属性,需要处理 if(this.getExecutors() != null){ List<String> _executors = new ArrayList<String>(); for(String s : this.getExecutors()){ _executors.add(s); } plan.setExecutors(_executors); } return plan; } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Date getStartdate() { return startdate; } public void setStartdate(Date startdate) { this.startdate = startdate; } public Date getEnddate() { return enddate; } public void setEnddate(Date enddate) { this.enddate = enddate; } public List<String> getExecutors() { return executors; } public void setExecutors(List<String> executors) { this.executors = executors; } public int getLevel() { return level; } public void setLevel(int level) { this.level = level; } @Override public String toString() { return "[name=" + name + ", level=" + level + ", startdate=" + startdate + ", enddate=" + enddate + ", executors=" + executors + "]"; } }
测试(没改动):
package com.designpattern.Prototype.deepcopy; import java.util.ArrayList; import java.util.List; import com.designpattern.utils.DateFormatUtil; /** * 测试 * @author Json<<json1990@foxmail.com>> */ public class Client { public static void main(String[] args) { List<String> executors = new ArrayList<String>(); executors.add("张三"); executors.add("李四"); Plan plan = new Plan(); plan.setName("重构前端登录界面"); plan.setLevel(1); plan.setStartdate(DateFormatUtil.stringToDate(DateFormatUtil.YYYYMMDD,"2017-08-07")); plan.setEnddate(DateFormatUtil.stringToDate(DateFormatUtil.YYYYMMDD,"2017-08-09")); plan.setExecutors(executors); Plan plan2 = plan.clone(); plan2.setName("后端接口改造"); plan2.setLevel(2); plan2.setStartdate(DateFormatUtil.stringToDate(DateFormatUtil.YYYYMMDD,"2017-08-10")); plan2.setEnddate(DateFormatUtil.stringToDate(DateFormatUtil.YYYYMMDD,"2017-08-12")); System.out.println("地址是否一样?"+(plan == plan2)); System.out.println("plan.getName() == plan2.getName() "+(plan.getName() == plan2.getName())); System.out.println("plan.getLevel() == plan2.getLevel() "+(plan.getLevel() == plan2.getLevel())); System.out.println("plan.getStartdate() == plan2.getStartdate() "+(plan.getStartdate() == plan2.getStartdate())); System.out.println("plan.getEnddate() == plan2.getEnddate() "+(plan.getEnddate() == plan2.getEnddate())); System.out.println("plan.getExecutors() == plan2.getExecutors() "+(plan.getExecutors() == plan2.getExecutors())); System.out.println("plan:"+plan.toString()); System.out.println("plan2:"+plan2.toString()); //plan任务比较重,在给plan添加一个人 executors.add("王五"); plan.setExecutors(executors); System.out.println(); System.out.println("地址是否一样?"+(plan == plan2)); System.out.println("plan.getName() == plan2.getName() "+(plan.getName() == plan2.getName())); System.out.println("plan.getLevel() == plan2.getLevel() "+(plan.getLevel() == plan2.getLevel())); System.out.println("plan.getStartdate() == plan2.getStartdate() "+(plan.getStartdate() == plan2.getStartdate())); System.out.println("plan.getEnddate() == plan2.getEnddate() "+(plan.getEnddate() == plan2.getEnddate())); System.out.println("plan.getExecutors() == plan2.getExecutors() "+(plan.getExecutors() == plan2.getExecutors())); System.out.println("plan:"+plan.toString()); System.out.println("plan2:"+plan2.toString()); } }
结果:
地址是否一样?false
plan.getName() == plan2.getName() false
plan.getLevel() == plan2.getLevel() false
plan.getStartdate() == plan2.getStartdate() false
plan.getEnddate() == plan2.getEnddate() false
plan.getExecutors() == plan2.getExecutors() false
plan:[name=重构前端登录界面, level=1, startdate=Mon Aug 07 00:00:00 CST 2017, enddate=Wed Aug 09 00:00:00 CST 2017, executors=[张三, 李四]]
plan2:[name=后端接口改造, level=2, startdate=Thu Aug 10 00:00:00 CST 2017, enddate=Sat Aug 12 00:00:00 CST 2017, executors=[张三, 李四]]
地址是否一样?false
plan.getName() == plan2.getName() false
plan.getLevel() == plan2.getLevel() false
plan.getStartdate() == plan2.getStartdate() false
plan.getEnddate() == plan2.getEnddate() false
plan.getExecutors() == plan2.getExecutors() false
plan:[name=重构前端登录界面, level=1, startdate=Mon Aug 07 00:00:00 CST 2017, enddate=Wed Aug 09 00:00:00 CST 2017, executors=[张三, 李四, 王五]]
plan2:[name=后端接口改造, level=2, startdate=Thu Aug 10 00:00:00 CST 2017, enddate=Sat Aug 12 00:00:00 CST 2017, executors=[张三, 李四]]
到这里,大家对原型模式的浅拷贝、深拷贝有了一定了解了吧!
4、优缺点
优点:
1、对客户端隐藏具体的实现类型:原型模式的客户端,只知道原型接口的类型,并不知道具体的实现类型,从而减少了客户端对这些具体实现类型的依赖。
2、在运行时动态改变具体的实现类型:原型模式可以在运行期间,由客户来注册符合原型接口的实现类型,也可以动态的改变具体的实现类型,看起来接口没有任何变化,但其实运行的已经是另外一个类实例了。因为克隆一个原型就类似于实例化一个类。
缺点:
深度克隆方法实现会比较困难:原型模式最大的缺点就在于每个原型的子类都必须实现clone的操作,尤其在包含引用类型的对象时,clone方法会比较麻烦,必须要能够递归的让所有的相关对象都要正确的实现克隆。
5、应用场景
1、一个系统想要独立于它想要使用的对象时,可以使用原型模式,让系统只面向接口编程,在系统需要新的对象的时候,可以通过克隆原型来得到;
2、需要实例化的类是在运行时刻动态指定时,可以使用原型模式,通过克隆原型来得到需要的实例;
3、创建新对象成本较大(如初始化需要占用较长的时间,占用太多的CPU资源或网络资源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改。
.......
6、扩展总结
Java中对象的创建:
clone就是复制,在Java中,clone方法被对象调用,所以会复制对象。所谓的复制对象,首先要分配一个和源对象同样大小的空间,在这个空间中创建一个新的对象。
那么在java语言中,有几种方式可以创建对象呢?
1、使用new操作符创建一个对象
2、使用clone方法复制一个对象
那么这两种方式有什么相同和不同呢?
▶ new操作符的本意是分配内存。程序执行到new操作符时,首先去看new操作符后面的类型,因为知道了类型,才能知道要分配多大的内存空间。分配完内存之后,再调用构造函数,填充对象的各个域,这一步叫做对象的初始化,构造方法返回后,一个对象创建完毕,可以把他的引用(地址)发布到外部,在外部就可以使用这个引用操纵这个对象。
▶ clone在第一步是和new相似的,都是分配内存,调用clone方法时,分配的内存和源对象(即调用clone方法的对象)相同,然后再使用原对象中对应的各个域,填充新对象的域,填充完成之后,clone方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部。
clone 方法需要满足的几个原则:
x.clone() != x,两个对象不是一个;
x.clone().getClass() == x.getClass(),两个对象的class是同一个;
x.clone().equals(x),两个应该满足equals()方法条件;
注意:
Object类的clone方法只会拷贝java中的8中基本类型以及他们的封装类型,另外还有String类型。对于数组、容器对象、引用对象等都不会拷贝,这就是浅拷贝。如果要实现深拷贝,必须将原型模式中的数组、容器对象、引用对象等另行拷贝。
PS: 要使用clone方法,类的成员变量上不要增加final关键字。
PS:深拷贝和浅拷贝建议不要混合使用,特别是是在涉及到类的继承,父类有多个引用的情况就非常的复杂,建议的方案是深拷贝和浅拷贝分开实现。
PS:java实现真正的深拷贝,可以通过实现Serializable接口用序列化的办法来实现。
PS:源码地址 https://github.com/JsonShare/DesignPattern/tree/master
PS:原文地址 http://www.cnblogs.com/JsonShare/p/7300124.html