我们一定对类的实例化比较熟悉,前面我们说的单例、还有3种工厂模式都是通过new
关键字来创建对象,下面我们来了解一种新的对象创建的方式。
1. 定义
原型模式也是一种创建型的设计模式,实现和原理总体比较简单,一句话总结呢,就是可以实现用已有的对象创建新的对象,而不是用类来实例化对象,这样可以起到提升效率的目的。
众所周知,类的实例化可以创建对象,但其实这是一个比较耗时耗力的工作,尤其是在大量实例化对象的业务场景下,可能会对系统的性能造成很大的影响。这时候,原型模式就可以很好的解决问题,用已有的对象来生成对象的副本,这里已有的对象就是原型对象
,副本对象就是拷贝对象
。这样对于那些有非常复杂的初始化的操作,或者是需要消耗大量资源的情况,原型模式的优势就体现出来了。
2. 代码实现
我们来举一个飞机大战的游戏例子,游戏的场是在手机屏幕上方,飞下来很多敌机,而我方战机只有一架,其中敌机的飞行轨迹必须是呈上下直线型的,我方战机可以上下左右移动。因为本篇我们学习原型模式,所以重点关照的是敌机
,我方战机其实可以通过前面讲的单例模式实现。
2.1 实例化方式
假设游戏过程有500架敌机出现,通常情况下,使用实例化创建对象的方式,代码实现可以这样。
public class EnemyPlain {
// x坐标
private int x;
// y坐标
private int y;
// 敌机固定x坐标,只能上下移动
public EnemyPlain(int x) {
this.x = x;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
// 手柄每调用一次setY方法,Y坐标加一
public void setY() {
y ++ ;
}
}
客户端获取敌机的方法如下:
public class Client {
public static void main(String[] args) {
// 500架敌机集合
List<EnemyPlain> enemyPlains = new ArrayList<>();
// 实例化500架敌机
for (int i = 0; i < 500; i++) {
// 随机出现在0~200的坐标内
EnemyPlain enemyPlain = new EnemyPlain(RandomUtil.randomInt(200));
enemyPlains.add(enemyPlain);
}
}
}
这种代码实现的方式是很常见的,但要命的是这500个对象在客户端初始化的时候就会被创建出来,500个对象会占用大量的堆内存空间,这还是定义的对象只有两个属性的前提下。另外,CPU本身就是很宝贵的资源,一次性实例化500个对象,本身也会消耗系统很大的系统资源,极端的情况下会造成游戏界面卡顿,造成不友好的用户体验,下面我们用原型模式来试一下。
2.2 原型模式
原型模式的代码实现,首先把原型类实现java.lang.Clone
接口,接着实现 clone()
方法。
// 1.实现java.lang.Clone接口
public class EnemyPlain implements Cloneable {
// x坐标
private int x;
// y坐标
private int y;
// 敌机固定x坐标,只能上下移动
public EnemyPlain(int x) {
this.x = x;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
// 手柄每调用一次setY方法,Y坐标加一
public void setY() {
y ++ ;
}
public void setX(int x) {
this.x = x;
}
// 重写克隆 clone 方法
@Override
public EnemyPlain clone() throws CloneNotSupportedException {
return (EnemyPlain)super.clone();
}
}
客户端获取原型拷贝副本代码实现:
public class ClientAno {
public static void main(String[] args) throws CloneNotSupportedException {
// 创建原型对象
EnemyPlain enemyPlain = new EnemyPlain(100);
// 存放500架敌机
List<EnemyPlain> list = new ArrayList<>();
// 克隆500架敌机
for (int i = 0; i < 500; i++) {
EnemyPlain clonePlain = enemyPlain.clone();
// 设置横坐标
clonePlain.setX(RandomUtil.randomInt(200));
list.add(clonePlain);
}
}
}
这里需要特别说明,clone()
方法并不是从Cloneable
接口实现来的,而是继承自java.lang.Object
对象。另外,一般在获取克隆对象的时候,可以借助工厂模式一块实现。
3. UML类图
下面,就以上面飞机大战这个游戏的这个例子,画一个原型模式的UML类图。
4. 深拷贝和浅拷贝
想使用原型模式,就必须涉及到深拷贝和浅拷贝这两个概念,也是我们在开发中使用这个设计模式最容易出问题的地方,那这两者有什么区别呢?
Java中的变量分为原始类型和引用类型的,浅拷贝只会拷贝原始类型的值,引用类型的值只是拷贝了引用的地址,而深拷贝则会拷贝原始类型的值,并且给引用类型的属性从新克隆一个新的对象,这个对象在堆中地址都会不一样了。我们来举个例子,有a这个原型对象,克隆了b这个副本对象,两个对象都有同一个 Date 类型的属性,如果修改了a对象的 x 属性的值,那么b对象的这个值也会被改变,这在某些场景中可能会出问题,所以要提防这个坑。使用深拷贝,可以参考hutool工具包的api,方便我们开发。
5. 总结
最后来一点总结:从类创建对象叫做实例化,从对象生成对象叫做克隆。究其本质,克隆操作时,java虚拟机会进行内存操作,通过拷贝原型对象的数据流生成新的副本对象,就不会触发一些复杂的操作(比如类加载、初始化等),所以效率是远远高于通过new
关键字所触发的实例化操作。