在我们的现实的生活中,有很多的东西都是大致类似的,只有少许的特征不一样,比如说杯子,可能同一个品牌的杯子,除了深度不一样之外,其他的无论是口径、直径等都是一致的;再比如我们同一个型号、同一个品牌的汽车,高配版、中配版、低配版,他们除了一些少许的特征不一样之外,其他的都是一样的。
我们以汽车(Car)为例子,我们来看一下一般情况下我们如何创建汽车的对象;
//汽车类
public class Car {
/**
* 品牌
*/
private String band;
/**
* 型号
*/
private String model;
/**
* 厂家
*/
private String manufacturer;
/**
* 长度
*/
private int length;
/*
* 宽度
*/
private int width;
/*
* 高度
*/
private int high;
/*
* 价格
*/
private double price;
public String getBand() {
return band;
}
public void setBand(String band) {
this.band = band;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public String getManufacturer() {
return manufacturer;
}
public void setManufacturer(String manufacturer) {
this.manufacturer = manufacturer;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHigh() {
return high;
}
public void setHigh(int high) {
this.high = high;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
//.. ...
}
不同的汽车对象除了不同型号的价格不一样之外,其他的内容都是一样的,按照我们一般的实现方式,如果我们现在要创建两个汽车对象,他们当中只有型号(model)属性和价格(price)属性不一样,其他都是一样的,那么我们一般情况下的实现方案如下:
public class TestDemo {
public static void main(String[] args) {
Car lowLevelCar = new Car();
lowLevelCar.setBand("布加迪");
lowLevelCar.setHigh(100);
lowLevelCar.setLength(500);
lowLevelCar.setManufacturer("德国大众");
lowLevelCar.setModel("威龙");
lowLevelCar.setPrice(500.00d);
lowLevelCar.setWidth(200);
Car middleLevelCar = new Car();
middleLevelCar.setBand("布加迪");
middleLevelCar.setHigh(100);
middleLevelCar.setLength(500);
middleLevelCar.setManufacturer("德国大众");
middleLevelCar.setModel("威航");
middleLevelCar.setPrice(400.00d);
middleLevelCar.setWidth(200);
}
}
我们仔细分析上面的代码不难看出,我们写了很多冗余的代码,因为我们两个汽车对象lowLevelCar和middleLevelCar,除了model和price两个属性不一致之外,其他的属性都是一致的,那么我们有没有一种方法能够在创建新的对象时,可以复制一份已经存在的对象的内容,然后对复制过来的内容稍加修改,最终得到我们想要的对象呢?比如上例中我们复制lowLevelCar对象的内容到middleLevelCar,然后修改一下model和price两个属性从而得到我们想要结果,同时又不影响原来的对象lowLevelCar;这就是我们本文需要讲的一种模式——原型模式;
1、原型模式(Prototype Pattern)的定义
原型模式指的是使用原型实例指定创建对象的种类, 并且通过拷贝原型实例来创建新的对象,那么原型对象就是指我们已经创建并存在的对象,原型模式是一种对象创建型模式。
原型模式的原理其实非常的简单,在创建新对象时,不通过new关键字直接创建,而是通过原型对象调用自身的拷贝方法——克隆(Clone),克隆出一个和原型对象类似的对象来实现新对象的创建过程。由于在软件系统中我们经常会遇到需要创建多个相同或者相似对象的情况, 因此原型模式在真实开发中的使用频率还是非常高的。
需要注意的是通过克隆方法所创建的对象是全新的对象, 它们在内存中拥有新的地址, 通常对克隆所产生的对象进行修改对原型对象不会造成任何影响, 每一个克隆对象都是相互独立的。 通过不同的方式修改可以得到一系列相似但不完全相同的对象。
2、原型模式(Prototype Pattern)包含的角色
在原型模式结构图中包含如下几个角色:
Prototype( 抽象原型类) : 它是声明克隆方法的接口, 是所有具体原型类的公共父类, 可以是抽象类也可以是接口, 甚至还可以是具体实现类。
ConcretePrototype( 具体原型类) : 它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
Client( 客户类) : 让一个原型对象克隆自身从而创建一个新的对象, 在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象, 再通过调用该对象的克隆方法即可得到多个相同的对象。 由于客户类针对抽象原型类Prototype编程, 因此用户可以根据需要选择具体原型类, 系统具有较好的可扩展性, 增加或更换具体原型类都很方便。
原型模式的核心在于如何实现克隆方法,而克隆又分为深克隆和浅克隆。 浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制。
在Java中,对象的克隆,我们通常都是通过Java语言提供的clone()方法来实现。学过Java语言的人都知道, 所有的Java类都继承自java.lang.Object。 事实上, Object类提供一个clone()方法, 可以将一个Java对象复制一份。 因此在Java中可以直接使用Object提供的clone()方法来实现对象的克隆, Java语言中的原型模式实现很简单。
一般而言, Java语言中的clone()方法满足以下几个特征:
对任何对象x, 都有x.clone() != x, 即克隆对象与原型对象不是同一个对象;
对任何对象x, 都有x.clone().getClass() == x.getClass(), 即克隆对象与原型对象的类型一样;
如果对象x的equals()方法定义恰当, 那么x.clone().equals(x)应该成立。
为了获取对象的一份拷贝, 我们可以直接利用Object类的clone()方法, 具体步骤如下:
在派生类中覆盖基类的clone()方法, 并声明为public;
在派生类的clone()方法中, 调用super.clone();
派生类需实现Cloneable接口。
此时, Object类相当于抽象原型类, 所有实现了Cloneable接口的类相当于具体原型类。
3、浅克隆
所谓浅克隆指的是被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。对于一个类,如果我们需要实现Clone方法,我们必须实现Cloneable接口,这是一个标记接口,如果我们不实现这个接口,在调用clone方法时会报CloneNotSupportedException异常,我们来看实例。
对于我们上述所说的Car类,我们实现Cloneable接口并重写clone()方法。
public class Car implements Cloneable {
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
//... ...
}
在我们的测试类中,我们就可以按照如下的实现方式来创建middleLevelCar对象
public class TestDemo {
public static void main(String[] args) throws CloneNotSupportedException {
Car lowLevelCar = new Car();
lowLevelCar.setBand("布加迪");
lowLevelCar.setHigh(100);
lowLevelCar.setLength(500);
lowLevelCar.setManufacturer("德国大众");
lowLevelCar.setModel("威龙");
lowLevelCar.setPrice(500.00d);
lowLevelCar.setWidth(200);
Car middleLevelCar = (Car) lowLevelCar.clone();
middleLevelCar.setPrice(400.00d);
middleLevelCar.setModel("威航");
System.out.println(middleLevelCar.getBand());
System.out.println(middleLevelCar.getLength());
System.out.println(middleLevelCar.getModel());
}
}
执行上面的代码,我们会得到下面的结果:
布加迪
500
威航
从我们的结果中就可以看出,我们除了只需要修改不同的属性之外,其他的任何属性值都是从已有的原型对象lowLevelCar中复制过来,这让我们节省了很多的代码。
接下来我们来看一种情况,因为我们的汽车是由很多零件组成,比如车轮、发动机、方向盘等等,现在在我们的Car类中,需要增加一个轮子(Wheel)的属性,轮子(Wheel)类如下:
public class Wheel {
/**
* 品牌
*/
private String band;
/**
* 厂家
*/
private String manufacturer;
public String getBand() {
return band;
}
public void setBand(String band) {
this.band = band;
}
public String getManufacturer() {
return manufacturer;
}
public void setManufacturer(String manufacturer) {
this.manufacturer = manufacturer;
}
//... ...
}
那么在我们的Car类中,我们增加一个wheel的属性如下:
public class Car implements Cloneable {
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
private Wheel wheel;
//.. ...
public Wheel getWheel() {
return wheel;
}
public void setWheel(Wheel wheel) {
this.wheel = wheel;
}
//.. ...
}
修改我们的测试方法如下:
public class TestDemo {
public static void main(String[] args) throws CloneNotSupportedException {
Wheel wheel = new Wheel();
wheel.setBand("米其林");
wheel.setManufacturer("米其林厂商");
Car lowLevelCar = new Car();
lowLevelCar.setWheel(wheel);
lowLevelCar.setBand("布加迪");
lowLevelCar.setHigh(100);
lowLevelCar.setLength(500);
lowLevelCar.setManufacturer("德国大众");
lowLevelCar.setModel("威龙");
lowLevelCar.setPrice(500.00d);
lowLevelCar.setWidth(200);
Car middleLevelCar = (Car) lowLevelCar.clone();
middleLevelCar.setPrice(400.00d);
middleLevelCar.setModel("威航");
middleLevelCar.getWheel().setBand("固特异");
System.out.println(lowLevelCar.getWheel().getBand());
System.out.println(middleLevelCar.getWheel().getBand());
}
}
我们执行程序,得到如下的结果:
固特异
固特异
很明显,上面的结果并不是我们想要的,我们克隆一个对象,在对新的对象中的属性做修改时,我们并不希望原型对象的属性发生任何变化,但是在我们对新对象的引用对象进行修改时,原型对象的相同属性也发生了变化,这证明新对象中的引用对象和原型对象中的引用对象是同一个对象,原型对象中的引用对象并没有复制新的一份给新对象,所以对于对象当中存在其他对象的引用的这种情况,我们需要深克隆,才能将对象引用的对象也克隆一份。
4、深克隆
所谓深克隆,指的是无论原型对象的成员变量是值类型还是引用类型, 都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。 简单来说,在深克隆中, 除了对象本身被复制外, 对象所包含的所有成员变量也将复制。
在Java语言中, 如果需要实现深克隆, 可以通过序列化(Serialization)等方式来实现。 序列化就是将对象写到流的过程, 写到流中的对象是原有对象的一个拷贝, 而原对象仍然存在于内存中。 通过序列化实现的拷贝不仅可以复制对象本身, 而且可以复制其引用的成员对象, 因此通过序列化将对象写到一个流中, 再从流里将其读出来, 可以实现深克隆。 需要注意的是能够实现序列化的对象其类必须实现Serializable接口, 否则无法实现序列化操作。 下面我们使用深克隆技术来实现Car和Wheel的复制,由于要将Wheel对象和Car对象都写入流中,因此两个类均需要实现Serializable接口。
Wheel类实现Serializable接口之后如下
public class Wheel implements Serializable{
private static final long serialVersionUID = 1L;
/**
* 品牌
*/
private String band;
/**
* 厂家
*/
private String manufacturer;
public String getBand() {
return band;
}
public void setBand(String band) {
this.band = band;
}
public String getManufacturer() {
return manufacturer;
}
public void setManufacturer(String manufacturer) {
this.manufacturer = manufacturer;
}
//... ...
}
Car类实现Serializable接口之后,就不在需要实现Cloneable接口了,我们自行定义一个深克隆方法来实现克隆即可,具体如下:
public class Car implements Serializable {
private static final long serialVersionUID = 1L;
protected Object deepClone() throws IOException, ClassNotFoundException {
//将对象写入流中
ByteArrayOutputStream bao=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bao);
oos.writeObject(this);
//将对象从流中取出
ByteArrayInputStream bis=new ByteArrayInputStream(bao.toByteArray());
ObjectInputStream ois=new ObjectInputStream(bis);
return (Car)ois.readObject();
}
//... ...
}
修改完成之后,我们可以调整一下测试类如下:
public class TestDemo {
public static void main(String[] args) throws ClassNotFoundException, IOException {
Wheel wheel = new Wheel();
wheel.setBand("米其林");
wheel.setManufacturer("米其林厂商");
Car lowLevelCar = new Car();
lowLevelCar.setWheel(wheel);
lowLevelCar.setBand("布加迪");
lowLevelCar.setHigh(100);
lowLevelCar.setLength(500);
lowLevelCar.setManufacturer("德国大众");
lowLevelCar.setModel("威龙");
lowLevelCar.setPrice(500.00d);
lowLevelCar.setWidth(200);
Car middleLevelCar = (Car) lowLevelCar.deepClone();
middleLevelCar.setPrice(400.00d);
middleLevelCar.setModel("威航");
middleLevelCar.getWheel().setBand("固特异");
System.out.println(lowLevelCar.getWheel().getBand());
System.out.println(middleLevelCar.getWheel().getBand());
}
}
运行之后我们得到如下结果:
米其林
固特异
结果是我们期望的结果,证明我们通过序列化的方式实现了对象的深克隆。
总结
原型模式作为一种快速创建大量相同或相似对象的方式, 在软件开发中应用较为广泛, 很多软件提供的复制(Ctrl+C)和粘贴(Ctrl+V)操作就是原型模式的典型应用, 下面对该模式的使用效果和适用情况进行简单的总结。
主要优点
当创建新的对象实例较为复杂时, 使用原型模式可以简化对象的创建过程, 通过复制一个已有实例可以提高新实例的创建效率。
原型模式提供了简化的创建结构, 工厂方法模式常常需要有一个与具体类等级结构相同的工厂等级结构, 而原型模式就不需要这样, 原型模式中对象的复制是通过封装在原型类中的克隆方法实现的, 无须专门的工厂类来创建对象。
可以使用深克隆的方式保存对象的状态, 使用原型模式将对象复制一份并将其状态保存起来, 以便在需要的时候使用( 如恢复到某一历史状态) , 可辅助实现撤销操作。
主要缺点
需要为每一个类配备一个克隆方法, 而且该克隆方法位于一个类的内部, 当对已有的类进行改造时, 需要修改源代码, 违背了“开闭原则”。
在实现深克隆时需要编写较为复杂的代码, 而且当对象之间存在多重的嵌套引用时, 为了实现深克隆, 每一层对象对应的类都必须支持深克隆, 实现起来可能会比较麻烦。
适用场景
创建新对象成本较大( 如初始化需要占用较长的时间, 占用太多的CPU资源或网络资源) , 新的对象可以通过原型模式对已有对象进行复制来获得, 如果是相似对象, 则可以对其成员变量稍作修改。
如果系统要保存对象的状态, 而对象的状态变化很小, 或者对象本身占用内存较少时, 可以使用原型模式配合备忘录模式来实现。
需要避免使用分层次的工厂类来创建分层次的对象, 并且类的实例对象只有一个或很少的几个组合状态, 通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。