胡杨林 2019-01-11
在说原型模式之前,我想说说原型的概念。
什么是原型呢?我们都复印过文件吧,比如说我们只用一份文件,就可以复印出无数的复印件,这个原文件就是我们的原型。在Java语言中,原型对象就是我们要复制的对象,这里就涉及到了复制的概念。那原型模式就是根据现有实例生成新实例的模式。
一、原型模式概述
在《JAVA与模式》中这样描述原型模式:
原型模式属于对象的创建模式。通过给出一个原型对象来指明所要创建的对象的类型,然后用复制这个原型对象的办法创建出更多同类型的对象。
二、原型模式的类图
既然是一种设计模式,肯定是已经沉淀过的设计思想,因此有固定的方法套路让我们参考。原型模式的类图如下:
这里涉及到三个角色:
(1)Client角色:客户端类,也就是我们的调用者,提出创建对象的请求。
(2)抽象原型(Prototype)类/接口:这里可以是抽象类,也可以是接口类,此角色定义了具体的原型类需要实现的接口(复制接口)。
(3)具体的原型类(ConcretePrototype):具体的原型类,实现了抽象原型角色类或者接口定义的复制接口,用来实现复制现有实例来生成新实例的方法。
三、原型设计模式举例
实现复制,我们需要让Prototype接口类继承Cloneable接口类。
可能有人会认为Cloneable接口类中存在clone方法,其实没有。Cloneable接口类中没有定义任何方法,我们可以看下jdk中java.lang包下的Cloneable接口类:
根据注释我们可以明白,一个类实现<code> Cloneable </ code>接口,只是向 java.lang.Object#clone()方法表明它对于该方法来说是合法的。换句话说,它只是被用来标记“可以使用Object对象的clone()方法进行复制”。这样的接口也被称为“标记接口”(marker interface)。
那如果我们不继承Cloneable 接口,直接使用Object#clone()方法会怎样?注释也说了,会抛出java.lang.CloneNotSupportedException异常。大家不信可以尝试下,编译时就会报错。就算你用try-catch包裹,运行时还会抛出异常。
下面我们举一例说明,假设我们现在有一个需求,就是要实现将一个字符串放在一个特定符号(如*)包围的方框中显示,或者加上下划线显示出来。
这里,我们扩展一下原有的原型设计模式,增加一个管理类,实现类似于spring中的注册机制,用来管理所有注册的原型实例。
我们的类图如下:
1、定义原型接口类,里面包含两个方法,一个是复制方法,一个是实现将一个字符串放在一个特定符号(如*)包围的方框中显示,或者加上下划线显示出来的方法。
public interface Product extends Cloneable{ Product createClone(); void use(); }
2、定义具体的实现了原型接口类方法的类
①定义MessageBox类,实现特殊修饰符包裹的字符串展示
package com.zhaodf.pattern.prototypePattern; public class MessageBox implements Product { //定义字符串的展示修饰符 private char decochar; public MessageBox(char decochar) { this.decochar = decochar; } public Product createClone() { Product p = null; try { //这里调用Object的clone方法进行浅拷贝 p = (Product)clone(); }catch (CloneNotSupportedException e){ e.printStackTrace();; } return p; } public void use(String s) { int length = s.length(); for (int i=0;i<length+4;i++){ System.out.print(decochar); } System.out.println(); System.out.print(decochar+" "+s+" "+decochar); System.out.println(); for (int i=0;i<length+4;i++){ System.out.print(decochar); } } }
②定义UnderLinePen类,实现在字符串下展示下划线的效果
package com.zhaodf.pattern.prototypePattern; public class UnderLinePen implements Product { //定义字符串的展示修饰符 private char ulchar; public UnderLinePen(char ulchar) { this.ulchar = ulchar; } public Product createClone() { Product p = null; try { //这里调用Object的clone方法进行浅拷贝 p = (Product)clone(); }catch (CloneNotSupportedException e){ e.printStackTrace();; } return p; } public void use(String s) { int length = s.length(); System.out.println("\""+s+"\""); for (int i=0;i<length+4;i++){ System.out.print(ulchar); } System.out.println(); } }
3、定义一个管理类,专门用来管理注册的原型实例
package com.zhaodf.pattern.prototypePattern; import java.util.HashMap; import java.util.Map; public class Manager{ //将注册的实例放在map中 private Map<String,Product> showCase = new HashMap<String,Product>(); //将原型实例注册在map中 public void register(String name,Product proto){ showCase.put(name,proto); } //根据原型实例创建新的复制对象 public Product createProduct(String name){ Product product = showCase.get(name); return product.createClone(); } }
4、定义客户端调用类
package com.zhaodf.pattern.prototypePattern; public class Client { public static void main(String[] args){ Manager manager = new Manager(); UnderLinePen ulPen = new UnderLinePen('-'); MessageBox mb = new MessageBox('*'); manager.register("ulpen",ulPen); manager.register("msgBox",mb); Product p1 = manager.createProduct("ulpen"); p1.use("Hello,World"); Product p2 = manager.createProduct("msgBox"); p2.use("Hello,World"); } }
最后输出的效果为:
四、原型模式的好处
1、效率快:原型模式是利用了Object类的clone实现的对象复制,因此相比于直接使用new,它不需要重复属性初始化的步骤。另外,由于clone方法是一个native方法,因此,复制的效率比我们去new对象来的更快。
2、解耦:原型模式增加一个原型对象管理类,可以与原型对象类实现解耦,从而实现类组件复用。比如在示例中,我们在管理类中直接根据名称获取了原型对象。
五、答疑
关于clone方法的使用解释:
1、想要调用clone方法,必须要实现Cloneable接口(不论是原型对象实现了Cloneable接口还是其父类实现了Cloneable接口都可以)。在clone方法的源码检测了是否实现了Cloneable接口,否则抛出异常。
源码如下:
// Check if class of obj supports the Cloneable interface.
// All arrays are considered to be cloneable (See JLS 20.1.5)
if (!klass->is_cloneable()) {//这里检查了是否实现了Cloneable接口,如果没实现,会抛出异常CloneNotSupportException。
ResourceMark rm(THREAD);
THROW_MSG_0(vmSymbols::java_lang_CloneNotSupportedException(), klass->external_name());
}
2、clone方法的返回值是复制出的新的实例。clone方法内部所进行的处理是分配与被复制对象相同的内存空间,接着将要复制的实例中的字段的值复制到所分配的内存空间中去,这一点可以使用对象比较去判断,复制出的实例和原型对象的内存地址不是同一个。
源码如下:
// Make shallow object copy
const int size = obj->size();//取对象大小
oop new_obj = NULL;
if (obj->is_javaArray()) {//如果是数组
const int length = ((arrayOop)obj())->length();//取长度
new_obj = CollectedHeap::array_allocate(klass, size, length, CHECK_NULL);//分配内存,写入元数据信息
} else {
new_obj = CollectedHeap::obj_allocate(klass, size, CHECK_NULL);//分配内存,写入元数据信息
}
3、面试的时候常会问,在使用clone方法进行复制的时候,常会有的一行代码是什么?当然是super.clone()。
4、clone方法进行实例复制时,常会涉及到的就是浅拷贝和深拷贝的概念(也有叫浅克隆和深克隆的)。
①浅拷贝是指只克隆了原型对象中基本类型数据和对象内引用类型变量的地址引用,它内部的引用类型实例变量还是指向原先的堆内存区域。
②深拷贝不仅拷贝原型对象中的基本数据类型内容,而且拷贝原型对象包含的引用类型对象的内容。这一点我们在浅拷贝和深拷贝介绍中详细阐述。