Java设计模式-享元模式(FlyWeight Pattern)
目录
- 什么是享元模式
- 享元模式实现方式
- JavaSE享元模式的应用
- Struts2享元模式的应用
享元模式有点类似单例模式,都是为了降低内存消耗,提高执行效率。
一、什么是享元模式
享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。
享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象。UML图如下:
- Flyweight(抽象享元类):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
- ConcreteFlyweight(具体享元类):它实现了抽象享元类,其实例称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
- UnsharedConcreteFlyweight(非共享具体享元类):并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
- FlyweightFactory(享元工厂类):享元工厂类用于创建并管理享元对象,它针对抽象享元类编程,将各种类型的具体享元对象存储在一个享元池中,享元池一般设计为一个存储“键值对”的集合(也可以是其他类型的集合),可以结合工厂模式进行设计;当用户请求一个具体享元对象时,享元工厂提供一个存储在享元池中已创建的实例或者创建一个新的实例(如果不存在的话),返回新创建的实例并将其存储在享元池中。
享元模式将对象的状态分为内部状态和外部状态:
- 内部状态
内部状态是对象可共享出来的信息,存储在享元对象内部并且不会随环境改变而改变,如用户的ID、Name,它们可以作为一个对象的动态附加信息,不必直接储存在具体某个对象中,属于可以共享的部分。
翻译为白话:类的属性,在生成对象后,该属性每个对象都不一样,那他就是内部状态,用户的ID是每个用户都不一样的。
- 外部状态
外部状态是对象得以依赖的一个标记,是随环境改变而改变的、不可以共享的状态,它是一批对象的统一标识,是唯一的一个索引值。
翻译为白话:类的属性,在生成对象后,该属性大部分对象或部分的值相同,那么这个属性就是外部状态,例如用户属于VIP用户,很多用户都属于VIP用户。
二、享元模式实现方式
写个坦克大战的案例,地图上有非常多的坦克,比如金色、银色、灰色等等,每个坦克的速度不同,但跑不出3个速度:1KM/s、5KM/s、10KM/s;无论坦克如何生成都是3个颜色乘以3个速度的种类的一种。
坦克会被创建、消灭,我们使用享元模式来模拟这个案例,将9类坦克放入工厂缓存,每次坦克被消灭后,直接从工厂取一个,就不需要再创建了。
坦克的属性有:ID、Name、Color、Speed、Gun;
外部属性:Color、Speed
内部属性:ID、Name、Gun
为什么Name不是外部属性?
因为我们需要从工厂缓存中拿到一个缓存对象,如果根据Name查找,那么需要100个坦克,缓存中便会有100个对象,这样便失去了享元模式的作用。所以从缓存中获取对象时使用的key,一定是外部属性,这个案例中,我们使用color+Speed,那么缓存中最多就只有9个对象,每次取出来修改下ID、Name、Gun就可以了。
首先分析下这个案例的UML图,如下图:
GameRole是抽象的享元类,里面放了内部状态,Tank类是具体的享元类,外部属性被封装为TankType,由于基地每次游戏就生成一次,所以是一个非享元类。接下来看下代码实现
package org.flyweight.version1;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
/**
* 实现一个坦克大战中坦克角色的创建
* 分析下:
* 游戏角色有共同的属性:颜色、速度、id、name、存活状态等
* 我们将颜色、速度作为外部属性;id、name、存活作为内部属性;
* 颜色往往代表了战力,黄色代表高战力、白色代表中等战力、灰色代表一般战力
* 享元模式:被共享的单元
*/
class TankType{
private String color;
private String speed;
public TankType(String color, String speed){
this.color = color;
this.speed = speed;
}
// 重写hashcode的生成方式,因为map的get方法获取key对应的value,会对key进行hashcode计算
@Override
public int hashCode() {
return this.color.hashCode() + this.speed.hashCode();
}
// 重写两个对象的比较方法
@Override
public boolean equals(Object obj) {
TankType t = (TankType) obj;
return getSpeed() == t.getSpeed() && getColor() == t.getColor();
}
// 省略了些get/set方法
}
/**
* 游戏角色:抽象的享元对象
*/
abstract class GameRole {
public String id;
public String name;
public boolean isLive = true;
public abstract void move();
// 省略了些get/set方法
}
class Tank extends GameRole{
private String color;
private String speed;
private String gun;
public Tank(TankType tankType){
setColor(tankType.getColor());
setSpeed(tankType.getSpeed());
}
@Override
public void move() {
System.out.println("坦克在移动");
}
public void openFire(){
System.out.println("坦克在开火");
}
// 私有的,不能被手动设置颜色
private void setColor(String color) {
this.color = color;
}
// 私有的,不能被手动设置速度
private void setSpeed(String speed) {
this.speed = speed;
}
// 省略了些get/set方法
}
/**
* 非享元对象:游戏中基地只有2个无需共享
*/
class Home extends GameRole{
public Home(String id,String name){
super();
super.id = id;
super.name = name;
}
@Override
public void move() {
System.out.println("基地不会移动");
}
}
class GameFactory{
private static Map<TankType,Tank> tanks = new HashMap<>();
public static Tank getTank(TankType tankType){
Tank tank = tanks.get(tankType);
if (tanks.get(tankType) == null){
System.out.println("【新创建】一个坦克,颜色为:" + tankType.getColor() + "速度为:" + tankType.getSpeed());
tank = new Tank(tankType);
tanks.put(tankType,tank);
return tank;
}
System.out.println("【获取】一个坦克,颜色为:" + tank.getColor() + ",速度为:" + tankType.getSpeed());
return tank;
}
public static Tank getTankWithOutFlyWeight(TankType tankType){
return new Tank(tankType);
}
// 基地只有2个没必要用享元模式,直接new对象即可
public static Home getHome(String team){
if (team == "红队"){
return new Home("1","红队基地");
}
return new Home("2","蓝队基地");
}
}
public class Test {
public static void main(String[] args) {
int times = 10;
// 不使用享元模式
long starTime = System.currentTimeMillis();
for (int i = 0; i < times; i++){
GameFactory.getTankWithOutFlyWeight(randomTankType());
}
long endTime = System.currentTimeMillis();
System.out.printf("不使用享元模式的速度:%d毫秒.\n",(endTime-starTime));
// 使用享元模式
// 初始化游戏
init();
long starTime1 = System.currentTimeMillis();
for (int i = 0; i < times; i++){
// 从工厂拿一个坦克
GameFactory.getTank(randomTankType());
//拿到坦克后应该设置ID、name等属性,然后放入地图,这里就不再增加这段代码了。
}
long endTime1 = System.currentTimeMillis();
System.out.printf("使用享元模式的速度:%d毫秒",(endTime1-starTime1));
}
// 初始化游戏角色,放入工厂缓存
public static void init(){
String colors[] = {"黄色","银色","灰色"};
String speeds[] = {"10M/S","5M/S","1M/S"};
String speed,color;
TankType tankType;
// 循环生成9个坦克对象
for (int i = 0; i < colors.length; i++){
color = colors[i];
for (int j = 0; j < speeds.length; j++){
speed = speeds[j];
tankType = new TankType(color,speed);
GameFactory.getTank(tankType);
}
}
// 生成两个的基地
GameFactory.getHome("红队");
GameFactory.getHome("蓝队");
}
// 模拟随机生成坦克类型,提供给工厂使用
public static TankType randomTankType(){
String colors[] = {"黄色","银色","灰色"};
String speeds[] = {"10M/S","5M/S","1M/S"};
Random random = new Random();
int randomColor = random.nextInt(3);
String color = colors[randomColor];
int randomSpeed = random.nextInt(3);
String speed = speeds[randomSpeed];
TankType tankType = new TankType(color,speed);
return tankType;
}
}
// 运行结果
不使用享元模式的速度:6毫秒.
【新创建】一个坦克,颜色为:黄色速度为:10M/S
【新创建】一个坦克,颜色为:黄色速度为:5M/S
【新创建】一个坦克,颜色为:黄色速度为:1M/S
【新创建】一个坦克,颜色为:银色速度为:10M/S
【新创建】一个坦克,颜色为:银色速度为:5M/S
【新创建】一个坦克,颜色为:银色速度为:1M/S
【新创建】一个坦克,颜色为:灰色速度为:10M/S
【新创建】一个坦克,颜色为:灰色速度为:5M/S
【新创建】一个坦克,颜色为:灰色速度为:1M/S
【获取】一个坦克,颜色为:银色,速度为:5M/S
【获取】一个坦克,颜色为:灰色,速度为:1M/S
【获取】一个坦克,颜色为:黄色,速度为:1M/S
【获取】一个坦克,颜色为:灰色,速度为:1M/S
【获取】一个坦克,颜色为:灰色,速度为:10M/S
【获取】一个坦克,颜色为:灰色,速度为:5M/S
【获取】一个坦克,颜色为:银色,速度为:1M/S
【获取】一个坦克,颜色为:灰色,速度为:10M/S
【获取】一个坦克,颜色为:银色,速度为:1M/S
【获取】一个坦克,颜色为:黄色,速度为:10M/S
使用享元模式的速度:1毫秒
具体的代码就不在细讲,都是比较简单的代码,值得注意的是两种模式的运行速度是完全不一样的,很显然享元模式的速度更加快,当创建的对象越多时,差距会更明显。
三、JavaSE享元模式的应用
JDK中Integer使用了享元模式,在此之前,先了解下Integer类,下面代码,运行结果是什么?
Integer i1 = 127;
Integer i2 = 127;
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i1 == i2);
System.out.println(i3 == i4);
System.out.println(i3.equals(i4));
int ii1 = 127;
int ii2 = 127;
int ii3 = 128;
int ii4 = 128;
System.out.println(ii1 == ii2);
System.out.println(ii3 == ii4);
// 运行结果
true
false
true
true
true
在Java中有3类比较方法:
- ==:判断值是否相等
- ===:是判断的是值及类型是否完全相等
- equals:如果没有重写Object类的equals,则equals就是使用==比较,如果子类重写了,那就需要看子类如何定义的equals,在前面的例子中我们重写了TankType类的equals方法,如下面代码
// 比对速度和颜色是否相同
@Override
public boolean equals(Object obj) {
TankType t = (TankType) obj;
return getSpeed() == t.getSpeed() && getColor() == t.getColor();
}
前面的代码中为什么i1 == i2为true、i3 == i4为false,i3.equals(i4)为true呢?需要理解2个概念
- ==如何比较两个Java对象?
- 什么是自动装箱(Autoboxing)和自动拆箱(Unboxing)?
在Java中表示一个int值,有2种方式
int i = 10;// 基本类型,属于八大数据类型,基本类型,也叫内置类型
Integer i = 10;// 包装器类型,非八大数据类型,Java对象
装箱:基本类型->包装器类型; 拆箱:包装器类型->基本类型;
装箱是怎么做的?
当Integer i = 10执行时,10本质上是int基本类型,现在要将它封装为包装器类型,这就将一个基本类型变为了复杂的Java对象;Integer i = 10执行时,底层执行了Integer i = Integer.valueOf(10);可以在Integer类的valueOf(int i)方法上断点即可观察到,valueOf(int i)函数代码如下
public static Integer valueOf(int i) {
// 如果i大于-128,小于等于127
if (i >= IntegerCache.low && i <= IntegerCache.high)
// 从缓存中取出对应的Integer对象,这里用的就是享元模式
return IntegerCache.cache[i + (-IntegerCache.low)];
// 封装为一个Integer对象
return new Integer(i);
}
// Integer的构造函数,内部维护了一个int类型变量,保存真实的int值
public Integer(int value) {
this.value = value;
}
// Integer的equals方法
public boolean equals(Object obj) {
// 如果是一个Integer对象,则比较
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
//如果根本就不是Integer对象,直接返回false
return false;
}
// intValue其实就是返回Integer对象内部的int类型的value
// 所以Integer的equals方法其实就是将内部的int类型的value进行了==比较
public int intValue() {
return value;
}
Java为基本数据类型提供了对应的包装器类型。如下:
基本数据类型 | 对应包装器类型 |
---|---|
int | Integer |
long | Long |
float | Float |
double | Double |
boolean | Boolean |
short | Short |
byte | Byte |
char | Character |
拆箱是怎么做的?
Integer i = 10;// 包装器类型,非八大数据类型,Java对象
int j = i; // 底层执行了i.intValue();
intValue方法前面已经展示过,其实就是返回Integer对象内部的int类型的value
现在回到前面的问题:==如何比较两个Java对象?举个例子
User user = new User("张三", 23); // name=张三, age=23
Java把内存分成两种,一种叫做栈内存,一种叫做堆内存
-
栈内存:一些基本类型的变量和对象的引用变量
-
堆内存:由new创建的对象和数组
当我们通过“==”来判定两个对象是否相等的时候,实际上是在判断两个局部变量存储的地址是否相同,换句话说,是在判断两个局部变量是否指向相同的对象。
在回头看下前面的代码
Integer i1 = 127;
Integer i2 = 127;
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i1 == i2);// true
System.out.println(i3 == i4);// false
System.out.println(i3.equals(i4)); //true
i3 == i4为false这个理解了,因为执行了 Integer.valueOf()方法,该方法通过new关键字返回了对象,而且==是对比的是是否指向相同对象,所以为false;
i3.equals(i4)本质上调用的是Integer的equals方法,方法内部调用intValue方法返回int型的值,相当于两个int值做==比较,所以为true;
i1 == i2为什么为true呢?前面代码注释中已经讲过了,在看一遍,如下代码
public static Integer valueOf(int i) {
// 如果i大于-128,小于等于127
if (i >= IntegerCache.low && i <= IntegerCache.high)
// 从缓存中取出对应的Integer对象,这里用的就是享元模式
// IntegerCache是Integer的内部类,会初始化cache数组
return IntegerCache.cache[i + (-IntegerCache.low)];
// 封装为一个Integer对象
return new Integer(i);
}
如果valueOf方法的参数i大于-128,小于等于127取的是缓存中的Integer对象,所以i1与i2取的是一个对象,==是对比的是是否指向相同对象,所以为true;
这就是Integer类的享元模式。
四、Struts2享元模式的应用
没发现使用享元模式,如果读者发现确实使用了原型模式,还请私信我修改,谢谢。