一 概述
1.1 定义
使用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象。
顾名思义,先搞一个原型对象出来,然后在这个原型对象的基础上修修补补再弄出一个新对象来。
- 原型模式属于创建型模式
- 一个已存在的对象(即原型),通过复制原型的方式来创建一个内部属性跟原型都一样的新的对象,这就是原型模式
- 原型模式的核心是 clone 方法,通过 clone 方法来实现对象的拷贝
1.2 使用场景
- 当一个对象的构建代价过高时。例如某个对象里面的数据需要访问数据库才能拿到,而我们却要多次构建这样的对象
- 当构建的多个对象,均需要处于某种原始状态时,就可以先构建一个拥有此状态的原型对象,其他对象基于原型对象来修改
1.3 UML 类图
角色说明:
- Prototype(抽象原型类、原型接口):抽象类或者接口,用来声明 clone 方法,在 Java 中,我们可以使用 JDK 自带的java.lang.Cloneable 接口来替代此接口
- ConcretePrototype1、ConcretePrototype2(具体原型类):实现了 Prototype 接口的原型对象,这个对象有个能力就是可以克隆自己。即要被复制的对象
- Client(客户端类):即要使用原型模式的地方
二 实现
2.1 Prototype(抽象原型类)
1、通常情况下,Prototype 是不需要我们去定义的。因为拷贝这个操作十分常用,Java 中提供了 Cloneable 接口来支持拷贝操作,它就是原型模式中的 Prototype。
2、当然,原型模式也未必非得去实现 Cloneable 接口,也有其他的实现方式。如下:
public interface Prototype {
Prototype copy();
}
2.2 创建具体原型类
这个类就是我们的原型类,准备被其他人克隆使用的,所以其实现 Prototype 接口,具备克隆的能力。根据业务需求,克隆可以是浅克隆,也可以是深克隆。
实现 Cloneable 接口:
// 具体原型类,卡片类
public class Card implements Cloneable {// 实现 Cloneable 接口,Cloneable 只是标识接口
private int num; // 卡号
private Spec spec = new Spec(); // 卡规格
public Card() {
System.out.println("Card 执行构造函数");
}
public void setNum(int num) {
this.num = num;
}
public void setSpec(int length, int width) {
spec.setLength(length);
spec.setWidth(width);
}
@Override
public String toString() {
return "Card{" +
"num=" + num +
", spec=" + spec +
'}';
}
// 重写 clone() 方法,clone() 方法不是 Cloneable 接口里面的,而是 Object 里面的
@Override
protected Card clone() throws CloneNotSupportedException {
System.out.println("clone时不执行构造函数");
return (Card) super.clone();
}
}
// 规格类,有长和宽这两个属性
public class Spec {
private int width;
private int length;
public void setLength(int length) {
this.length = length;
}
public void setWidth(int width) {
this.width = width;
}
@Override
public String toString() {
return "Spec{" +
"width=" + width +
", length=" + length +
'}';
}
}
2.3 创建客户端类
即要使用原型模式的地方:
public class Client {
public void test() throws CloneNotSupportedException {
Card card1 = new Card();
card1.setNum(9527);
card1.setSpec(10, 20);
System.out.println(card1.toString());
System.out.println("----------------------");
Card card2 = card1.clone();
System.out.println(card2.toString());
System.out.println("----------------------");
}
}
输出结果:
Card 执行构造函数
Card{num=9527, spec=Spec{width=20, length=10}}
----------------------
clone时不执行构造函数
Card{num=9527, spec=Spec{width=20, length=10}}
----------------------
说明:
- clone 对象不会执行构造函数
- clone 方法不是 Cloneable 接口中的,而是 Object 中的方法。Cloneable 是个标识接口,表面了这个对象是可以拷贝的,如果没有实现 Cloneable 接口却调用 clone 方法则会报错
但是,如果执行下面的代码:
Card card1 = new Card();
card1.setNum(9527);
card1.setSpec(10, 20);
System.out.println(card1.toString());
System.out.println("----------------------");
Card card2 = card1.clone();
System.out.println(card2.toString());
System.out.println("----------------------");
card2.setNum(7259);
System.out.println(card1.toString());
System.out.println(card2.toString());
System.out.println("----------------------");
card2.setSpec(30, 40);
System.out.println(card1.toString());
System.out.println(card2.toString());
System.out.println("----------------------");
其输出结果为:
Card 执行构造函数
Card{num=9527, spec=Spec{width=20, length=10}}
----------------------
clone时不执行构造函数
Card{num=9527, spec=Spec{width=20, length=10}}
----------------------
Card{num=9527, spec=Spec{width=20, length=10}}
Card{num=7259, spec=Spec{width=20, length=10}}
----------------------
Card{num=9527, spec=Spec{width=40, length=30}}
Card{num=7259, spec=Spec{width=40, length=30}}
----------------------
我们会发现,修改了拷贝对象的引用类型(即 Spec 字段)时,原来的对象的值也跟着改变了;但是修改基本对象(num 字段)时,原来的对象的值却不会改变。这就涉及到深拷贝和浅拷贝了。
三 深拷贝和浅拷贝
3.1 浅拷贝
上面的例子实际上就是一个浅拷贝,如下图所示:
由于 num 是基本数据类型,因此直接将整数值拷贝过来就行。但是 spec 是 Spec 类型的, 它只是一个引用,指向一个真正的 Spec 对象,那么对它的拷贝有两种方式:
- 浅拷贝
- 深拷贝
其中直接将源对象中的 spec 的引用值拷贝给新对象的 spec 字段,这种拷贝方式就叫浅拷贝。
接下来看什么是深拷贝,如下:
3.2 深拷贝
另外一种拷贝方式就是根据原 Card 对象中的 spec 指向的对象创建一个新的相同的对象,将这个新对象的引用赋给新拷贝的 Card 对象的 spec 字段。这种拷贝方式就叫深拷贝,如下图所示:
3.3 原型模式改造
那么,我们对上面原型模式的例子进行改造,使其实现深拷贝,这就需要在 Card 的 clone 方法中,将源对象引用的 Spec 对象也 clone 一份。
public static class Card implements Cloneable {
private int num;
private Spec spec = new Spec();
public Card() {
System.out.println("Card 执行构造函数");
}
public void setNum(int num) {
this.num = num;
}
public void setSpec(int length, int width) {
spec.setLength(length);
spec.setWidth(width);
}
@Override
public String toString() {
return "Card{" +
"num=" + num +
", spec=" + spec +
'}';
}
@Override
protected Card clone() throws CloneNotSupportedException {
System.out.println("clone时不执行构造函数");
Card card = (Card) super.clone();
// 对 spec 对象也调用 clone,实现深拷贝
card.spec = (Spec) spec.clone();
return card;
}
}
// Spec 也实现 Cloneable 接口
public static class Spec implements Cloneable {
private int width;
private int length;
public void setLength(int length) {
this.length = length;
}
public void setWidth(int width) {
this.width = width;
}
@Override
public String toString() {
return "Spec{" +
"width=" + width +
", length=" + length +
'}';
}
// 重写 Spec 的 clone 方法
@Override
protected Spec clone() throws CloneNotSupportedException {
return (Spec) super.clone();
}
}
测试代码:
Card card1 = new Card();
card1.setNum(9527);
card1.setSpec(10, 20);
System.out.println(card1.toString());
System.out.println("----------------------");
Card card2 = card1.clone();
System.out.println(card2.toString());
System.out.println("----------------------");
card2.setNum(7259);
System.out.println(card1.toString());
System.out.println(card2.toString());
System.out.println("----------------------");
card2.setSpec(30, 40);
System.out.println(card1.toString());
System.out.println(card2.toString());
System.out.println("----------------------");
其输出结果为:
Card 执行构造函数
Card{num=9527, spec=Spec{width=20, length=10}}
----------------------
clone时不执行构造函数
Card{num=9527, spec=Spec{width=20, length=10}}
----------------------
Card{num=9527, spec=Spec{width=20, length=10}}
Card{num=7259, spec=Spec{width=20, length=10}}
----------------------
Card{num=9527, spec=Spec{width=20, length=10}}
Card{num=7259, spec=Spec{width=40, length=30}}
----------------------
由此可见,card1 和 card2 内的 spec 引用指向了不同的 Spec 对象, 也就是说在 clone Card 对象的同时,也拷贝了它所引用的 Spec 对象, 进行了深拷贝。
四 总结
4.1 应用场景
- 如果初始化一个类时需要耗费较多的资源,比如数据、硬件等等,可以使用原型拷贝来避免这些消耗
- 通过 new 创建一个新对象时如果需要非常繁琐的数据准备或者访问权限,那么也可以使用原型模式
- 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以拷贝多个对象供调用者使用,即保护性拷贝
4.2 优点
- 可以解决复杂对象创建时消耗过多的问题,在某些场景下提升创建对象的效率
- 保护性拷贝,可以防止外部调用者对对象的修改,保证这个对象是只读的
4.3 缺点
- 拷贝对象时不会执行构造函数
- 有时需要考虑深拷贝和浅拷贝的问题
- 相对增加了系统的复杂性
五 Android 中的源码实例分析
Android 中的 Intent 就实现了 Cloneable 接口,但是 clone() 方法中却是通过 new 来创建对象的。
public class Intent implements Parcelable, Cloneable {
// 其他代码略
@Override
public Object clone() {
// 这里没有调用 super.clone() 来实现拷贝,而是直接通过 new 来创建
return new Intent(this);
}
public Intent(Intent o) {
this.mAction = o.mAction;
this.mData = o.mData;
this.mType = o.mType;
this.mPackage = o.mPackage;
this.mComponent = o.mComponent;
this.mFlags = o.mFlags;
this.mContentUserHint = o.mContentUserHint;
if (o.mCategories != null) {
this.mCategories = new ArraySet<String>(o.mCategories);
}
if (o.mExtras != null) {
this.mExtras = new Bundle(o.mExtras);
}
if (o.mSourceBounds != null) {
this.mSourceBounds = new Rect(o.mSourceBounds);
}
if (o.mSelector != null) {
this.mSelector = new Intent(o.mSelector);
}
if (o.mClipData != null) {
this.mClipData = new ClipData(o.mClipData);
}
}
}
总结 :
实际上,调用 clone() 构造对象时并不一定比 new 快,使用 clone() 还是 new 来创建对象需要根据构造对象的成本来决定,如果对象的构造成本比较高或者构造比较麻烦,那么使用 clone() 的效率比较高,否则使用 new。