Java设计模式之享元模式
Java设计模式之享元模式
在说享元模式之前来先看一道题:
public static void main(String[] args) {
Integer i1 = new Integer(50);
Integer i2 = new Integer(50);
System.out.println(i1 == i2);
Integer i3 = new Integer(500);
Integer i4 = new Integer(500);
System.out.println(i3 == i4);
//需要注意下面这种方式存在隐式装箱
Integer i5 = 50;
Integer i6 = 50;
System.out.println(i5 == i6);
Integer i7 = 500;
Integer i8 = 500;
System.out.println(i7 == i8);
}
很简单对不对?
答案
false
false
true
false
这便是我想说的享元模式。
享元模式英文为:Flyweight,《JAVA与模式》一书中开头是这样描述享元(Flyweight)模式的:
Flyweight在拳击比赛中指最轻量级,即“蝇量级”或“雨量级”,这里选择使用“享元模式”的意译,是因为这样更能反映模式的用意。享元模式是对象的结构模式。享元模式以共享的方式高效地支持大量的细粒度对象。
享元即为分享元素,字符串常量池、数据库连接池、缓冲池都是是这个道理。该模式的意图为:运用共享技术有效地支持大量细粒度的对象。
就像上边的例子中,Integer类会把较小的数字保存起来,再次新建比较小的Integer对象时会直接返回该对象的引用,从而避免了再次创建对象。通过查看Integer类的源码我们可以看到Integer会把[-128, 127]之间的数字直接返回共享池中的对象:
public static Integer valueOf(int i) {
//IntegerCache.low = -128
//IntegerCache.high = 127
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
这也是为什么上面例子中第三个输出true的原因。
单纯享元模式
在单纯的享元模式中,所有的享元对象都是可以共享的。
单纯享元模式所涉及到的角色如下:
- 抽象享元(Flyweight)角色 :父接口,以规定出所有具体享元角色需要实现的方法。
- 具体享元(ConcreteFlyweight)角色:实现抽象享元角色所规定出的接口。
- 享元工厂(FlyweightFactory)角色 :本角色负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当地共享。当一个客户端对象调用一个享元对象的时候,享元工厂角色会检查系统中是否已经有一个符合要求的享元对象。如果已经有了,享元工厂角色就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个合适的享元对象。
上面的Integer例子就是单纯的享元模式
到这里可能有同学会问了:这种引用类型的分享对于final的String和Integer来说倒无所谓,因为被final修饰之后不能再改变,所以如何分享引用都没关系。但是清楚基本类型和引用类型的差别都知道引用类型传引用之后,改变对象的内部数据会导致对象被修改:
int[] a = {1, 2};
int[] b = a;
b[0] = 20;
System.out.println(a[0]);//同为20
这样该怎么办呢?
别担心,你想到的享元模式也想到了。
符合享元模式
先来认识两个概念:
内部状态:在享元对象内部不随外界环境改变而改变的共享部分。
外部状态:随着环境的改变而改变,不能够共享的状态就是外部状态。
由于享元模式区分了内部状态和外部状态,所以我们可以通过设置不同的外部状态使得相同的对象可以具备一些不同的特性,而内部状态设置为相同部分。在我们的程序设计过程中,我们可能会需要大量的细粒度对象来表示对象,如果这些对象除了几个参数不同外其他部分都相同,这个时候我们就可以利用享元模式来大大减少应用程序当中的对象。
我们举一个最简单的例子,棋牌类游戏大家都有玩过吧,比如说说围棋和跳棋,它们都有大量的棋子对象,围棋和五子棋只有黑白两色,跳棋颜色略多一点,但也是不太变化的,所以棋子颜色就是棋子的内部状态;而各个棋子之间的差别就是位置的不同,我们落子嘛,落子颜色是定的,但位置是变化的,所以方位坐标就是棋子的外部状态。
那么为什么这里要用享元模式呢?可以想象一下,上面提到的棋类游戏的例子,比如围棋,理论上有361个空位可以放棋子,常规情况下每盘棋都有可能有两三百个棋子对象产生,因为内存空间有限,一台服务器很难支持更多的玩家玩围棋游戏,如果用享元模式来处理棋子,那么棋子对象就可以减少到只有两个实例,这样就很好的解决了对象的开销问题。
复合享元模式UML:
复合享元角色所涉及到的角色如下:
- 抽象享元(Flyweight)角色 :父接口,以规定出所有具体享元角色需要实现的方法。
- 具体享元(ConcreteFlyweight)角色:实现抽象享元角色所规定出的接口。
- 复合享元(ConcreteCompositeFlyweight)角色 :复合享元角色所代表的对象是不可以共享的,但是一个复合享元对象可以分解成为多个本身是单纯享元对象的组合。复合享元角色又称作不可共享的享元对象。
- 享元工厂(FlyweightFactory)角色 :本角 色负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当地共享。当一个客户端对象调用一个享元对象的时候,享元工厂角色会检查系统中是否已经有 一个符合要求的享元对象。如果已经有了,享元工厂角色就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个 合适的享元对象。
下面就用下棋来举例:
抽象享元类:
//棋子类
public abstract class AbstractChessman {
// 棋子坐标
protected int x;
protected int y;
// 棋子类别(黑|白)
protected String chess;
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("--BlackChessman Construction Exec!!!");
}
// 点坐标设置
@Override
public void point(int x, int y) {
this.x = x;
this.y = y;
// 显示棋子内容
show();
}
}
白色棋子:
//白色棋子
public class WhiteChessman extends AbstractChessman {
/**
* 构造方法 初始化白棋子
*/
public WhiteChessman() {
super("○");
System.out.println("--WhiteChessman Construction Exec!!!");
}
// 点坐标设置
@Override
public void point(int x, int y) {
this.x = x;
this.y = y;
// 显示棋子内容
show();
}
}
享元工厂:
棋子工厂类:
import java.util.Hashtable;
//棋子工厂
public class ChessmanFactory {
// 单例模式工厂
private static ChessmanFactory chessmanFactory = new ChessmanFactory();
// 缓存存放共享对象
private final Hashtable<Character, AbstractChessman> cache = new Hashtable<>();
// 私有化构造方法
private ChessmanFactory() {
}
// 获得单例工厂
public static ChessmanFactory getInstance() {
return chessmanFactory;
}
/**
* 根据字符获得棋子
*
* @param c (B:黑棋 W:白棋)
* @return
*/
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:
break;
}
// 为防止 非法字符的进入 返回null
if (abstractChessman != null) {
// 放入缓存
this.cache.put(c, abstractChessman);
}
}
// 如果缓存中存在 棋子对象则直接返回
return abstractChessman;
}
}
客户端类,即测试类
import java.util.Random;
//测试类
public class Client {
public static void main(String[] args) {
// 创建五子棋工厂
ChessmanFactory fiveChessmanFactory = 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 = fiveChessmanFactory.getChessmanObject('B');
break;
// 获得白棋
case 1:
abstractChessman = fiveChessmanFactory.getChessmanObject('W');
break;
}
if (abstractChessman != null) {
abstractChessman.point(i, random.nextInt(15));
}
}
}
}
运行结果:
--BlackChessman Construction Exec!!!
●(0,3)
●(1,0)
--WhiteChessman Construction Exec!!!
○(2,1)
●(3,12)
●(4,4)
●(5,9)
●(6,9)
○(7,2)
●(8,11)
○(9,6)
ps:复合享元不可共享,只需继承抽象享元即可,不再演示。
小结
享元模式:
- 意图:运用共享技术有效地支持大量细粒度的对象。
- 主要解决:在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。
- 何时使用:
- 系统中有大量对象。
- 这些对象消耗大量内存。
- 这些对象的状态大部分可以外部化。
- 这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替。
- 系统不依赖于这些对象身份,这些对象是不可分辨的。
- 如何解决:用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象。
- 关键代码:用 HashMap 存储这些对象。
- 应用实例:
- Integer类,在[-128, 127]大小范围内将直接返回相同的对象
- JAVA 中的 String,如果有则返回,如果没有则创建一个字符串保存在字符串缓存池里面。
- 数据库的数据池。
- 优点:大大减少对象的创建,降低系统的内存,使效率提高。
- 缺点:提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。
- 使用场景:
- 系统有大量相似对象。
- 需要缓冲池的场景。
- 注意事项:
- 注意划分外部状态和内部状态,否则可能会引起线程安全问题。
- 这些类必须有一个工厂对象加以控制