原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。
原型模式的定义与特点
模式的定义:
用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。
在这里,原型实例指定了要创建的对象的种类。用这种方式创建对象非常高效,根本无须知道对象创建的细节。
模式目的:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
使用背景:
1、当一个系统应该独立于它的产品创建,构成和表示时。
2、当要实例化的类是在运行时刻指定时,例如,通过动态装载。
3、为了避免创建一个与产品类层次平行的工厂类层次时。
4、当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。
模式优点:
1、性能提高。
2、逃避构造函数的约束。
模式缺点:
1、配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。
2、必须实现 Cloneable 接口。
使用场景:
1.在需要一个类的大量对象的时候,使用原型模式是最佳选择,因为原型模式是在内存中对这个对象进行拷贝,要比直接new这个对象性能要好很多,在这种情况下,需要的对象越多,原型模式体现出的优点越明显。
2.如果一个对象的初始化需要很多其他对象的数据准备或其他资源的繁琐计算,那么可以使用原型模式。
3.当需要一个对象的大量公共信息,少量字段进行个性化设置的时候,也可以使用原型模式拷贝出现有对象的副本进行加工处理。
注意事项:与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的。浅拷贝实现 Cloneable,重写,深拷贝是通过实现 Serializable 读取二进制流。
原型模式的实现过程中,要知道两个概念:
- 浅复制:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用都仍然指向原来的对象。
- 深复制:把引用对象的变量指向复制过来的新对象,而不是原有的被引用的对象。
原型模式的结构与实现
由于Java提供了对象的 clone() 方法,所以用 Java 实现原型模式很简单。
1. 模式的结构
原型模式包含以下主要角色。
- 抽象原型类:规定了具体原型对象必须实现的接口。
- 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
- 访问类:使用具体原型类中的 clone() 方法来复制新的对象。
其结构图如图 所示。
首先我们可以看到一共有三个角色:
(1)客户(Client)角色:客户类提出创建对象的请求。
(2)抽象原型(Prototype)角色:规定了具体原型对象必须实现的接口(如果要提供深拷贝,则必须具有实现clone的规定)。
(3)具体原型(Concrete Prototype)角色:从抽象原型派生而来,是客户程序使用的对象,即被复制的对象,需要实现抽象原型角色所要求的接口。
我们会发现其实原型模式的核心就是Prototype(抽象原型),他需要继承Cloneable接口,并且重写Object类中的clone方法才能有克隆的功能。
2. 模式的实现
原型模式的克隆分为浅克隆和深克隆,Java 中的 Object 类提供了浅克隆的 clone() 方法,具体原型类只要实现 Cloneable 接口就可实现对象的浅克隆,这里的 Cloneable 接口就是抽象原型类。
既然我们知道原型模式的核心就是拷贝对象,那么我们能拷贝一个对象实例的什么内容呢?这就要区分深拷贝和浅拷贝之分了。
(1)浅拷贝:我们只拷贝对象中的基本数据类型(8种),对于数组、容器、引用对象等都不会拷贝。
(2)深拷贝:不仅能拷贝基本数据类型,还能拷贝那些数组、容器、引用对象等。
第一步:定义抽象原型
public class Prototype implements Cloneable {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public Prototype clone () {
try {
return (Prototype) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}
第二步:定义具体原型
public class ConcretePrototype extends Prototype{
private String type;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public void show(){
System.out.println("具体原型信息展示:" + this.getName() + "," + this.getAge() + "," + this.getType());
}
}
第三步:定义用户去模拟过程
public class Client {
public static void main(String[] args) {
Prototype prototype = new Prototype();
prototype.setName("原型类");
prototype.setAge(18);
System.out.println("创建原型基类:" + prototype);
//克隆
Prototype clone = prototype.clone();
System.out.println("复制克隆类:" + clone);
//判断原型类和克隆类的属性
System.out.println("基类==克隆类:" + (clone == prototype));
//改变克隆类属性值
clone.setName("克隆类");
System.out.println("复制克隆类-修改后:");
System.out.println("复制克隆类:" + clone);
System.out.println("创建原型基类:" + prototype);
}
}
我们可以看到,克隆出来的两个文件和之前的文件信息是一样的,但是是不同的对象。上述例子只展示了浅拷贝,对于数组、引用等对象则不适用。具体可以在基类中声明一个引用类型的对象,在对原型实例对象的改引用对象做修改的时候,其克隆类中该属性的值也会相应改变。
具体的实例可以移步 ——设计模式之原型模式
原型模式的优缺点
优点:
A、原型模式对客户隐藏了具体的产品类
B、运行时刻增加和删除产品: 原型模式允许只通过客户注册原型实例就可以将一个新的具体产品类并入系统。
C、改变值以指定新对象: 高度动态的系统允许通过对象复合定义新的行为。如通过为一个对象变量指定值并且不定义新的类。通过实例化已有类并且将实例注册为客户对象的原型,就可以有效定义新类别的对象。客户可以将职责代理给原型,从而表现出新的行为。
D、改变结构以指定新对象:许多应用由部件和子部件来创建对象。
E、减少子类的构造,Prototype模式克隆一个原型而不是请求一个工厂方法去产生一个新的对象。
F、用类动态配置应用 一些运行时刻环境允许动态将类装载到应用中。
G、使用原型模式创建对象比直接new一个对象在性能上要好的多,因为Object类的clone方法是一个本地方法,直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显。
H、使用原型模式的另一个好处是简化对象的创建,使得创建对象很简单。
缺点:
原型模式的主要缺陷是每一个抽象原型Prototype的子类都必须实现clone操作,实现clone函数可能会很困难。当所考虑的类已经存在时就难以新增clone操作,当内部包括一些不支持拷贝或有循环引用的对象时,实现克隆可能也会很困难的。
原型模式的应用场景
原型模式通常适用于以下场景。
- 对象之间相同或相似,即只是个别的几个属性不同的时候。
- 对象的创建过程比较麻烦,但复制比较简单的时候。
原型模式的扩展
原型模式可扩展为带原型管理器的原型模式,它在原型模式的基础上增加了一个原型管理器 PrototypeManager 类。该类用 HashMap 保存多个复制的原型,Client 类可以通过管理器的 get(String id) 方法从中获取复制的原型。其结构图如图 所示。