享元模式(FlyWeight),属于结构型设计模式,主要解决实例化大量相同的对象,从而导致可能的内存泄漏的问题。
为了解决这个问题,享元模式提出的解决办法是将相同的对象保存在内存中,且仅保存一个对象,因此该对象应该是不可被修改的,当需要获取该对象实例时,直接从内存中读取即可,从而避免了相同对象的重复创建。
下面是享元模式的定义:
运用共享技术有效地支持大量细粒度的对象。
享元模式有以下四个基本角色:
享元工厂(FlyWeightFactory)
工厂模式的应用,用来创建并管理享元对象,根据某一标准,在完成享元对象实例化以后,判断是否需要对该实例对象进行管理。如果客户端需要从享元工厂中获取某些实例,享元工厂将会判断是否对相同的实例进行管理,如果存在被管理的相同的实例对象,则无需重复对其进行实例化,而是直接返回被管理的对象。
一般来说,享元工厂通过HashMap
对需要共享的实例进行管理。
抽象享元(FlyWeight)
享元类的抽象接口,规定了对象的行为。
共享的具体享元(SharedFlyWeightImpl)
实现于抽象享元接口,对其规定的行为进行具体实现。并且该类的实例对象在由享元工厂实例化以后,需要被工厂管理,以便在以后需要相同的对象时直接返回,而不是重复实例化。
非共享的具体享元(UnsharedFlyWeightImpl)
实现于抽象享元接口,对其规定的行为进行具体实现。与共享的具体享元实例不同的是,非共享的具体享元在被享元工厂实例化以后,不被工厂管理,即每一次需要该实例时,都需要享元工厂重复创建。
享元模式的通用UML类图如下所示
基于以上通用UML类图,我们通过代码对其进行演示
新建**抽象享元(Flyweight)**接口
public interface Flyweight {
void doSomething();
}
新建**共享的具体享元(SharedFlyweightImpl)**类
public class SharedFlyweightImpl implements Flyweight{
@Override
public void doSomething() {
System.out.println("共享的享元对象:doSomething()方法,hash值:" + hashCode());
}
}
新建**非共享的具体享元(UnsharedFlyweightImpl)**类
public class UnsharedFlyweightImpl implements Flyweight{
@Override
public void doSomething() {
System.out.println("非共享的享元对象:doSomething()方法,hash值:" + hashCode());
}
}
新建**享元工厂(FlyweightFactory)**类
在享元工厂中,我们使用**Map对象cache
**模拟缓存,由工厂创建并管理的实例都保存在该cache
中。
如果我们需要的享元实例是以share_
开头,说明该实例是共享的享元实例,则需要从缓存中判断是否存在对应的实例,如果存在,则说明该实例已被创建且被管理,直接返回即可。如果不存在,则创建后保存到缓存cache
并返回该实例。
如果我们需要的享元实例是以unshare_
开头,说明该实例是非共享的实例,工厂则需要重复创建实例并返回,而无需保存到缓存。
public class FlyweightFactory {
private static final Map<String, Flyweight> cache = new HashMap<>();
public static Flyweight create(String flyweightKey) {
Flyweight flyweight = null;
if (flyweightKey.startsWith("share_")) {
flyweight = cache.get(flyweightKey);
if (flyweight == null) {
flyweight = new SharedFlyweightImpl();
cache.put(flyweightKey, flyweight);
}
} else if (flyweightKey.startsWith("unshare_")) {
flyweight = new UnsharedFlyweightImpl();
}
return flyweight;
}
}
新建**客户端(FlyweightClient)**类
public class FlyweightClient {
public static void main(String[] args) {
// 第一个share_instance1实例
Flyweight first = FlyweightFactory.create("share_instance1");
// 第二个share_instance1实例
Flyweight second = FlyweightFactory.create("share_instance1");
first.doSomething();
second.doSomething();
System.out.println(first == second); // 输出为true,说明两个实例对象为同一个,来自缓存
System.out.println();
Flyweight instance2 = FlyweightFactory.create("share_instance2");
instance2.doSomething();
System.out.println(first == instance2);
System.out.println();
// 第一个unshare_instance3实例
Flyweight third = FlyweightFactory.create("unshare_instance3");
// 第二个unshare_instance3实例
Flyweight forth = FlyweightFactory.create("unshare_instance3");
third.doSomething();
forth.doSomething();
System.out.println(third == forth); // 输出为true,说明两个实例对象不是同一个,重复创建
}
}
运行以上代码,可得以下结果
我们以给新生儿起名字为例,在我们社会中,比如张伟、晓燕、小宝,这些名字到处可见,而嬴政、玄烨、乔治这样的名字又少之又少。从代码的角度来看,如果我们在每次需要起张伟这个名字的时候,都去创建一个实例,那么对内存占用无疑是一种不友好的行为;而起嬴政、玄烨、乔治这样的名字时,由于使用这种名字的人本来就不多,也就无所谓我们重复创建了。
所以我们使用享元模式来模拟该案例。
Name
public interface Name {
}
NameImpl
该类中使用属性name
保存姓名,该属性通过构造方法设置。
public class NameImpl implements Name{
private final String name;
public NameImpl(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
NameFactory
在该工厂中,有一个保存常用姓名的字段commonNameList
,使用这些名字的姓名实例Name
在创建实例后由缓存cache
管理,再次需要相同的名字实例时从缓存cache
中获取即可,避免了相同实例的重复创建;而不是常用名字的情况,则需要重复创建实例。
public class NameFactory {
// 常用的名字
private static final List<String> commonNameList = Arrays.asList("ZhangWei", "XiaoYan", "XiaoBao");
// Map模拟缓存
private static Map<String, Name> cache = new HashMap<>();
public static Name create(String nameStr) {
Name name = null;
if (commonNameList.contains(nameStr)) {
name = cache.get(nameStr);
if (name == null) {
name = new NameImpl(nameStr);
cache.put(nameStr, name);
}
} else {
name = new NameImpl(nameStr);
}
return name;
}
}
NameClient
public class NameClient {
public static void main(String[] args) {
Name zhangWei_1 = NameFactory.create("ZhangWei");
Name zhangWei_2 = NameFactory.create("ZhangWei");
// 如果为true,说明ZhangWei是从缓存中获取的共享实例,避免了实例的重复创建
System.out.println("如果为true,说明ZhangWei是从缓存中获取的共享实例,避免了实例的重复创建");
System.out.println(zhangWei_1 == zhangWei_2);
System.out.println();
Name xiaoYan_1 = NameFactory.create("XiaoYan");
Name xiaoYan_2 = NameFactory.create("XiaoYan");
System.out.println("如果为true,说明XiaoYan是从缓存中获取的共享实例,避免了实例的重复创建");
System.out.println(xiaoYan_1 == xiaoYan_2);
System.out.println();
System.out.println("如果为false,说明ZhangWei和XiaoYan是来自缓存的两个不同实例");
System.out.println(zhangWei_1 == xiaoYan_1);
System.out.println();
Name yingZheng_1 = NameFactory.create("YingZheng");
Name yingZheng_2 = NameFactory.create("YingZheng");
System.out.println("如果为false,说明名字为YingZheng的两个实例yingZheng_1和yingZheng_2是重复创建的两个实例");
System.out.println(yingZheng_1 == yingZheng_2);
}
}
运行该案例,输出以下结果
享元模式在jdk中广泛应用,如Integer.valueOf()
和 Byte.valueOf()
,看过源码的同学都知道,在Integer.valueOf()
中,如果我们传入的参数是-128 ~ 127
之间的整数,那么Integer
会从IntegerCache
中返回一个已经创建好的实例。同样的,在Byte.valueOf()
中,由于一个字节只有8个bit
,其范围是从-128~ 127
,共256个整数,因此在jvm启动时,就会预先创建好这256个整数对应的Byte实例。
下面我们看源码解析
首先查看Integer.valueOf()
方法
// 其中IntegerCache.low = -128,IntegerCache.high = 127
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
从该方法中,我们看到,如果传入的参数位于-128和127
之间,则从IntegerCache
的cache
数组中返回。
下面我们来看IntegerCache
的源码。该类中有一个静态代码块,在jvm启动时就会立刻执行该代码块。
在该代码块中,通过for循环
对cache
数组进行初始化,数组中每一个元素都是-128和127
之间的Integer实例。
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 =
sun.misc.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() {}
}
同样进入Byte.valueOf()
方法
public static Byte valueOf(byte b) {
final int offset = 128;
return ByteCache.cache[(int)b + offset];
}
从该源码中看到,直接从ByteCache
的cache
数组中返回对应的Byte实例
下面我们查看ByteCache
的源码
与IntegerCache
类似,在静态代码块中通过for循环
直接对cache
数组进行初始化,该数组中每一个元素都是对应的Byte
对象。
private static class ByteCache {
private ByteCache(){}
static final Byte cache[] = new Byte[-(-128) + 127 + 1];
static {
for(int i = 0; i < cache.length; i++)
cache[i] = new Byte((byte)(i - 128));
}
}
从设计的角度来讲,享元模式与单例模式的区别似乎都应该属于创建型设计模式,因为他们的目的都是为了避免频繁创建相同的对象。
从使用者的角度来讲,他们之间又有很大的不同,通过单例模式获取的对象实例,并没有对我们修改实例做出限制,而通过享元模式获取的对象实例,我们是无法对其做出修改的,从Integer
和Byte
中的应用我们也可以看出,他们都是被final
所修饰的,不允许修改。
优点:
缺点:
纸上得来终觉浅,绝知此事要躬行。
————————我是万万岁,我们下期再见————————