Flyweight design pattern
享元模式的概念、享元模式的结构、享元模式的优缺点、享元模式的使用场景、享元模式的实现示例、享元模式的源码分析
1、享元模式的概念
享元模式,即运用共享技术来有效的支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量,避免大量相似对象的开销,从而提高系统资源的利用率。
享元模式中存在以下两种状态:
- 内部状态:即不会随着环境的改变而改变的可共享状态。
- 外部状态:即随着环境的改变而改变的不可共享状态。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。
2、享元模式的结构
- 抽象享元角色:定义向外界提供内部状态的抽象方法,同时声明外界设置外部状态的的方法。
- 具体享元角色:继承自抽象享元角色,实现向外界提供内部状态的抽象方法。
- 飞享元角色:即外部状态,即不可共享的部分,可以参数的形式注入到享元对象中去。
- 享元工厂:负责创建和管理享元对象。当客户对象请求一个享元对象时,享元工厂检查系统中是否已存在符合要求的享元对象,若存在则返回给用户,若不存在则创建一个新的享元对象返回给客户并维护到系统中。因为系统中享元角色的创建和管理只需一个角色,故此工厂可设计位单例。
3、享元模式的优缺点
- 优点:
- 减少相似对象的创建,降低系统内存开销,提高效率。
- 缺点:
- 一定程度上增加了系统的复杂度,需要分离出内部状态和外部状态,内部状态不变,外部状态可变,容易造成系统混乱。
4、享元模式的使用场景
- 当系统中存在大量相同或相似的对象时,避免造成系统内存大量耗费。
- 当对象的大部分状态可外部化时,可将这些外部状态传入对象中。
- 在使用享元模式时,需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,故,应在需要多次重复使用享元对象时才值得使用享元模式。
5、享元模式的实现示例
抽象享元角色:
public abstract class Hero {
/**
* 向外界提供不可变内部状态(可共享)
* @return
*/
public abstract String getName();
/**
* 外界设置可变外部状态(不可共享)
* @param equipment
*/
public void equipment(String equipment) {
System.out.println("英雄名称 " + this.getName() + " 装备 " + equipment);
}
}
具体享元角色:
public class ZedHero extends Hero {
@Override
public String getName() {
return "影流之主";
}
}
具体享元角色:
public class FizzHero extends Hero {
@Override
public String getName() {
return "潮汐海灵";
}
}
具体享元角色:
public class AhriHero extends Hero {
@Override
public String getName() {
return "九尾妖狐";
}
}
享元工厂:
public class FlyWeightFactory {
private static Map<String, Hero> map;
private static FlyWeightFactory factory;
private FlyWeightFactory() {
map = new HashMap<>(3);
map.put("zed", new ZedHero());
map.put("fizz", new FizzHero());
map.put("ahri", new AhriHero());
}
public static FlyWeightFactory getInstance() {
if (factory == null) {
synchronized (FlyWeightFactory.class) {
if (factory == null) {
factory = new FlyWeightFactory();
}
}
}
return factory;
}
public Hero getHero(String name) {
return map.get(name);
}
}
测试:
public class FlyWeightTest {
public static void main(String[] args) {
Hero zed = FlyWeightFactory.getInstance().getHero("zed");
zed.equipment("德拉克撒的慕刃");
Hero fizz = FlyWeightFactory.getInstance().getHero("fizz");
fizz.equipment("卢登的激荡");
Hero zed1 = FlyWeightFactory.getInstance().getHero("zed");
zed1.equipment("赛瑞尔达的怨恨");
System.out.println(zed == zed1);
}
}
测试结果:
英雄名称 影流之主 装备 德拉克撒的慕刃
英雄名称 潮汐海灵 装备 卢登的激荡
英雄名称 影流之主 装备 赛瑞尔达的怨恨
true
6、享元模式的源码分析
jdk 的 Integer 类的设计就使用到了享元模式。
public class IntegerTest {
public static void main(String[] args) {
Integer a = 127;
Integer b = 127;
Integer c = 128;
Integer d = 128;
System.out.println(a == b);
System.out.println(c == d);
}
}
// 测试结果
true
false
从测试结果结合 Integer 源码可知,Integer 对 -128 ~ 127 之间的数进行了缓存,所以对象 a、b 实际上是同一个对象,因为其被换存在内存中了,而对象 c、d 则是两个不同的对象,因为其没有被缓存。
Integer a = 127; 这段代码反编译后实际上会变成 Integer a = Integer.valueOf((int) 127); 也就是实际上是调用了 Integer 的 valueOf() 方法。
@HotSpotIntrinsicCandidate
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
从 Integer.valueOf() 的源码可知,Integer 内部用了一个 IntegerCache 内部类来维护要被缓存的数值范围即被缓存的对象数组,可以看到在 low(-128) 到 high(127)之间的数值被缓存在 cache[] 中了。在调用 Integer.valueOf() 方法时,如果传入的值在 low 和 high 之间,则之间返回数组中的元素,若不在则创建一个新的对象返回。