享元模式(Flyweight Pattern):以共享的方式高效的支持大量的细粒度对象。通过复用内存中已存在的对象,降低系统创建对象实例的性能消耗。
享元即为共享元对象,在面向对象中,创建对象是很消耗资源的,享元模式就是为避免产生大量的细粒度对象提供解决方案的。
享元模式有两种状态,内蕴状态和外蕴状态,内蕴状态存储于享元对象内部,不会随环境而改变,可以共享;外蕴状态随外部环境改变,并且由客户端保存,不能共享。
享元模式类图:
涉及到的角色如下:
抽象享元:为具体享元类规定出公共接口;
具体享元:实现抽象享元类接口,如果有内蕴状态,需要为内蕴状态提供存储空间,这是保证共享的必要条件;
享元工厂:负责创建和管理享元角色,保证享元对象可以被共享,例如客户端获取享元对象时,需要判断该类型对象是否存在,存在则直接返回,不存在则新建然后返回;
客户端:需要维护对所需享元对象的引用,如果有外蕴状态,需要将外蕴状态保存在客户端;
模式案例
五子棋中会用到很多的黑子和白子,如果对于每一个黑子或白子都创建一个对象会太消耗内存。可以使用享元模式,使得在整个游戏中只有“黑子”和“白子”两个对象。
首先创建一个棋子抽象类作为棋子的超类,含有一个棋子标识的属性:
/**
* 棋子的超类,含有一个棋子类别的属性,标志具体的棋子类型
*/
public abstract class AbstractChessman {
//棋子类别
protected String chess;
//构造方法
public AbstractChessman(String chess){
this.chess = chess;
}
//显示棋子信息
public void show(){
System.out.println(this.chess);
}
}
黑子类:
/**
* 需求:黑子类
*/
public class BlackChessman extends AbstractChessman {
/*
* 构造方法,初始化黑棋子
*/
public BlackChessman(){
super("●");
System.out.println("--一颗黑棋子诞生了!--");
}
}
白子类:
/**
* 需求:白棋子
*
*/
public class WhiteChessman extends AbstractChessman {
/*
* 构造方法,初始化黑棋子
*/
public WhiteChessman(){
super("○");
System.out.println("--一颗白棋子诞生了!--");
}
}
下面来设计棋子工厂类,棋子工厂类我们设计为单例模式,该类用来生产棋子对象实例,并放入缓存当中,下次再获得棋子对象的时候就从缓存当中获得。内容如下:
/**
* 需求:棋子工厂,用于生产棋子对象实例,并放入缓存中,采用单例模式完成
*/
public class ChessmanFactory {
//单例模式
private static ChessmanFactory chessmanFactory = new ChessmanFactory();
//缓存共享对象
private final Hashtable<Character, AbstractChessman> cache = new Hashtable<Character, AbstractChessman>();
//构造方法私有化
private ChessmanFactory(){
}
//获得单例工厂对象
public static ChessmanFactory getInstance(){
return chessmanFactory;
}
/*
* 根据字母获得棋子
*/
public AbstractChessman getChessmanObject(char c){
//从缓存中获得棋子对象实例
AbstractChessman abstractChessman = this.cache.get(c);
//判空
if (abstractChessman==null) {
//说明缓存中没有该棋子对象实例,需要创建
switch (c) {
case 'B':
abstractChessman = new BlackChessman();
break;
case 'W':
abstractChessman = new WhiteChessman();
break;
default:
System.out.println("非法字符,请重新输入!");
break;
}
//如果有非法字符,那么对象必定仍为空,所以再进行判断
if (abstractChessman!=null) {
//放入缓存
this.cache.put(c, abstractChessman);
}
}
//如果缓存中存在棋子对象则直接返回
return abstractChessman;
}
}
通过客户端进行测试:
public class Test {
public static void main(String[] args) {
//创建工厂
ChessmanFactory chessmanFactory = ChessmanFactory.getInstance();
//随机数,用于生成棋子对象
Random random = new Random();
int radom = 0;
AbstractChessman abstractChessman = null;
//随机获得棋子
for (int i = 0; i < 10; i++) {
radom = random.nextInt(2);
switch (radom) {
case 0:
//获得黑棋子
abstractChessman = chessmanFactory.getChessmanObject('B');
break;
case 1:
//获得黑棋子
abstractChessman = chessmanFactory.getChessmanObject('W');
break;
}
if (abstractChessman!=null) {
abstractChessman.show();
}
}
}
}
执行后,我们发现“一颗黑棋子诞生了!”和“一颗白棋子诞生了!”各执行了一次,说明在众多棋子中只有一个黑棋子和一个白棋子,实现了对象的共享,这就是享元。
外蕴状态
上边例子使用到了内蕴状态,内蕴状态对于任何一个享元对象来讲是完全相同的,可以说,内蕴状态保证了享元对象能够被共享。例如上边的“黑子”和“白子”,代表的状态就是内蕴状态。
如果我们为棋子添加位置信息,因为棋子位置都是不一样的,是不能够共享的,这需要使用外蕴状态。享元对象的外蕴状态与内蕴状态是两类相互独立的状态,彼此没有关联。
增加棋子位置信息的抽象类为:
/**
* 需求:棋子的超类,含有一个棋子类别的属性,标志具体的棋子类型
*/
public abstract class AbstractChessman {
//棋子类别
protected String chess;
//棋子坐标
protected int x;
protected int y;
//构造方法
public AbstractChessman(String chess){
this.chess = chess;
}
//坐标设置
public abstract void point(int x,int y);
//显示棋子信息
public void show(){
System.out.println(this.chess+"("+this.x+","+this.y+")");
}
}
完善后的黑子类:
/**
* 需求:黑子类
*/
public class BlackChessman extends AbstractChessman {
/*
* 构造方法,初始化黑棋子
*/
public BlackChessman(){
super("●");
System.out.println("--一颗黑棋子诞生了!--");
}
/*
* 重写方法
*/
@Override
public void point(int x, int y) {
this.x = x;
this.y = y;
this.show();
}
}
完善后的白子类为:
/**
* 需求:白棋子
*/
public class WhiteChessman extends AbstractChessman {
/*
* 构造方法,初始化黑棋子
*/
public WhiteChessman(){
super("○");
System.out.println("--一颗白棋子诞生了!--");
}
/*
* 重写方法
*/
@Override
public void point(int x, int y) {
this.x = x;
this.y = y;
this.show();
}
}
在棋子工厂中不需要进行任何改变,因为在棋子工厂中我们获得的是共享对象,外蕴状态(位置信息)是需要在客户端进行设置的。
客户端:
public class Test {
public static void main(String[] args) {
//创建工厂
ChessmanFactory chessmanFactory = ChessmanFactory.getInstance();
//随机数,用于生成棋子对象
Random random = new Random();
int radom = 0;
AbstractChessman abstractChessman = null;
//随机获得棋子
for (int i = 0; i < 10; i++) {
radom = random.nextInt(2);
switch (radom) {
case 0:
//获得黑棋子
abstractChessman = chessmanFactory.getChessmanObject('B');
break;
case 1:
//获得黑棋子
abstractChessman = chessmanFactory.getChessmanObject('W');
break;
}
if (abstractChessman!=null) {
abstractChessman.point(i, random.nextInt(15));
}
}
}
}
需要注意的是,为了保证对象被共享,外蕴状态需要保存在客户端,也就是说,x、y值在客户端保存。
享元对象特点
享元模式的重点在于“共享对象实例,降低内存空间”,实现享元模式时,需要规划共享对象的粒度,才能降低内存空间,提高系统性能;例如如果将上边的x、y转为内蕴状态,那么享元工厂将保存非常多的对象,失去享元模式的意义。
当系统中某个对象类型的实例较多且耗费大量内存,并且对象实例分类较少,且其部分状态可以转为外蕴状态时,可以使用享元模式。单例模式本身也是一种特殊的享元模式,最大区别是单例模式的类不能被实例化,而享元模式类可以被实例化。
享元模式的缺点是,为了使对象可以共享,可能会将一些状态转为外蕴状态,使得程序复杂性增加。
Java中的享元模式
Integer类对于经常使用的-128到127范围内的Integer对象,当类被加载时就被创建了,并保存在cache数组中,一旦程序调用valueOf方法,如果i的值是在-128到127之间就直接在cache缓存数组中去取Integer对象而不是创建一个新对象,这就是享元模式的应用。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
String类也应用了享元模式,常量池中维护的字符串可以被多个指针引用。
参考:《设计模式那点事》