在有些系统中,存在大量相同或相似对象的创建问题,如果用传统的构造函数来创建对象, 会比较复杂且耗时耗资源,用原型模式生成对象就很高效,就像孙悟空拔下猴毛轻轻一吹就变出很多孙悟空一样简单。
模式的定义
原型(Prototype)模式的定义:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。在这里,原型实例指定了要创建的对象的种类。用这种方式创建对象非常高效,根本无须知道对象创建的细节。
模式的结构与实现
结构
由于 Java 提供了对象的 clone() 方法,所以用 Java 实现原型模式很简单。
原型模式包含以下主要角色:
- 抽象原型类:规定了具体原型对象必须实现的接口。
- 具体原型类:实现抽象原型类的clone() 方法,它是可被复制的对象。
- 使用类:使用具体原型类中的 clone() 方法来复制新的对象。
实现
原型模式的克隆分为浅克隆和深克隆,Java 中的 Object 类提供了浅克隆的 clone() 方法,具体原型类只要实现 Cloneable 接口就可实现对象的浅克隆,这里的 Cloneable 接口就是抽象原型类。使用了克隆的方式产生新的对象,新生成的对象也可以 根据需要稍做修改以适应需求。
这个时候有同学就会问为什么 Object 里提供了 clone 方法,为什么还需要实现 Cloneable 接口。
Java语言虽然提供了这个方法,但考虑到安全问题,一方面将 clone() 方法的访问级别设置为 protected,以限制外部类访问。另一方面,强制需要提供 clone 功能的子类实现 java.lang.Cloneable 接口。在运行期,JVM 会检查调用 clone() 方法的类,如果该类未实现 java.lang. Cloneable 接口。则抛出 CloneNotSupportedException 异常。
public class Goods implements Cloneable{
public String name;
public Date date;
public Goods(String name,Date date) {
this.name = name;
this.date = date;
}
@Override
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode())+ "=>>Goods{" +
"name='" + name + '\'' +
", date=" + date +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {
Goods a = new Goods("面包",new Date());
Goods clone = (Goods) a.clone();
System.out.println(a);
System.out.println(clone);
System.out.println(a.date==clone.date);
System.out.println(a==clone);
}
}
上面代码简单实现了原型模式的克隆。我们可以看下运行结果:
com.yuxuan.learning.design.patterns.creative.prototype.Goods@3cd1f1c8=>>Goods{name='面包', date=Tue Aug 1 14:27:17 CST 2012}
com.yuxuan.learning.design.patterns.creative.prototype.Goods@6979e8cb=>>Goods{name='面包', date=Tue Aug 1 14:27:17 CST 2012}
goods==clone? false
goods.date==clone.date?true
由结果可见,生成后的对象和源对象在内存中的确是两个不同的地址,而且生成后的对象与源对象中的属性是完全一样的。
但是,仔细一看会发现一个问题,就是虽然新对象和源对象不是一样的对象,但是其中的属性对象和源对象的属性对象共享一个对象,比如上面的 date 属性。
浅克隆与深克隆
浅克隆与深克隆的区别:
- 浅克隆:被复制的所有变量都具有与原来对象相同的值,而所有的对其他对象的引用都仍然指向原来的对象。
- 深克隆:把引用对象的变量指向复制过的新对象,而不是原有的被引用的对象。
很显然上面实现的是一个浅克隆,因为克隆出来的对象的 date 属性和源对象的 date 属性指向的还是同一个对象地址。
下面我们调整一下 clone 方法里的代码,具体如下:
public class Goods implements Cloneable{
public String name;
public Date date;
public Goods(String name,Date date) {
this.name = name;
this.date = date;
}
@Override
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode())+ "=>>Goods{" +
"name='" + name + '\'' +
", date=" + date +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
Goods goods = (Goods)super.clone();
goods.date = (Date) this.date.clone();
return goods;
}
public static void main(String[] args) throws CloneNotSupportedException {
Goods goods = new Goods("面包",new Date());
Goods clone = (Goods) goods.clone();
System.out.println(goods);
System.out.println(clone);
System.out.println("goods==clone? "+(goods==clone));
System.out.println("goods.date==clone.date?"+(goods.date==clone.date));
}
}
我们在来看一下运行结果过:
com.yuxuan.learning.design.patterns.creative.prototype.Goods@3cd1f1c8=>>Goods{name='面包', date=Tue Aug 1 14:46:25 CST 2012}
com.yuxuan.learning.design.patterns.creative.prototype.Goods@6979e8cb=>>Goods{name='面包', date=Tue Aug 1 14:46:25 CST 2012}
goods==clone? false
goods.date==clone.date?false
从运行结果我们不难看出,现在 date 属性指向的不是同一个对象了。现在就是深克隆了。
需要注意的是,clone 方法只会进行复制,并不会调用被复制实例的构造函数。对于在生成实例时需要进行特殊的初始化处理的类,需要自己在 clone 方法中进行处理。
总结
原型模式为我们提供了另外一种高效创建对象的方法。使用原型模式,我们可以不了解原型对象的任何细节以及它内部 的层次的结构,不影响,但是必须要实现特定的接口。当需要的对象需要从现有的对象中复制时,通常适合使用原型模式。
原型模式和工厂模式的区别 :
- 原型模式可以选择维护一个产品的原型对象,并在方法中返回原型对象的克隆。
- 工厂模式直接返回新的产品对象,此对象是根据类中的代码新创建的。
原型模式也有它的缺点。每一个原型的子类都必须实现 Clone 操作,这可能会很困难。 例如,当所使用此模式的类已经存在时就难以新增 Clone 操作。当内部包括一些不支持拷贝或由循环引用的对象时,实现克隆可能也会很困难。