1、什么是享元模式?
享元模式(Flyweight Pattern):以共享的方式高效的支持大量的细粒度对象。通过复用内存中已存在的对象,降低系统创建对象实例的性能消耗。
享元的英文是Flyweight,是一个来自体育方面的专业用语,在拳击、摔跤和举重比赛中特指最轻量的级别。把这个单词移植到软件工程中,也是用来表示特别小的对象,即细粒度的对象。至于为什么把Flyweight翻译为“享元”,可以理解为共享元对象,也就是共享细粒度对象。
在面向对象中,大量细粒度对象的创建、销毁及存储所造成的资源和性能上的损耗,可能会在系统运行时形成瓶颈。那么该如何避免产生大量的细粒度对象,同时又不影响系统使用面向对象的方式进行操作呢?享元模式提供了一个比较好的解决方案。
2、享元模式类图:
享元模式又分为内蕴状态和外蕴状态,接下来将使用案例进行分析。
3、案例
案例需求:在五子棋中,会用到很多的黑子和白子,但是对于每一个黑子或白子都创建一个对象的话,那么会太过消耗内存。我们能不能共享对象实例呢?使得在整个游戏中只有“黑子”和“白子”两个对象。这就需要使用享元模式。
首先创建一个棋子抽象类作为棋子的超类,含有一个棋子标识的属性:
<span style="font-size:18px;">
/**
* 需求:棋子的超类,含有一个棋子类别的属性,标志具体的棋子类型
* @author 猛龙过江
*
*/
public abstract class AbstractChessman {
//棋子类别
protected String chess;
//构造方法
public AbstractChessman(String chess){
this.chess = chess;
}
//显示棋子信息
public void show(){
System.out.println(this.chess);
}
}</span>
<span style="font-size:18px;">
/**
* 需求:黑子类
* @author 猛龙过江
*
*/
public class BlackChessman extends AbstractChessman {
/*
* 构造方法,初始化黑棋子
*/
public BlackChessman(){
super("●");
System.out.println("--一颗黑棋子诞生了!--");
}
}</span>
<span style="font-size:18px;">
/**
* 需求:白棋子
* @author 猛龙过江
*
*/
public class WhiteChessman extends AbstractChessman {
/*
* 构造方法,初始化黑棋子
*/
public WhiteChessman(){
super("○");
System.out.println("--一颗白棋子诞生了!--");
}
}</span>
<span style="font-size:18px;">
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
/**
* 需求:棋子工厂,用于生产棋子对象实例,并放入缓存中,采用单例模式完成
* @author 猛龙过江
*
*/
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;
}
}</span>
<span style="font-size:18px;">import java.util.Random;
/**
* 需求:客户端(测试类)
* @author 猛龙过江
*
*/
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();
}
}
}
}</span>
4、需求改了
我们还需要改动需求,因为棋子必须有位置,所以我们还需要让棋子显示位置。显然,棋子对象是可以共享的,但是棋子位置都是不一样的,是不能够共享的,这久涉及到了享元模式的两种状态:内蕴状态(Internal State)和外蕴状态(External State)。
内蕴状态:
享元对象的内蕴状态是不会随环境的改变而改变的,是存储在享元对象内部的状态信息,因此内蕴状态是可以共享的,对于任何一个享元对象来讲,它的值是完全相同的。就想上边的“黑子”和“白子”,它代表的状态就是内蕴状态。
外蕴状态:
享元对象的第二类状态就是外蕴状态,它会随着环境的改变而改变,因此是不可以共享的状态,对于不同的享元对象来说,它的值可能是不同的。享元对象的外蕴状态必须由客户端保存,在享元对象被创建之后,需要使用的时候再传入到享元对象内部,就像五子棋的位置信息,代表的就是享元对象的外蕴状态。
所以,享元对象的外蕴状态与内蕴状态是两类相互独立的状态,彼此没有关联。
5、实现外蕴状态
外蕴状态变量是需要随着环境的变化而改变的,我们需要在抽象棋子类中增加棋子位置即坐标信息,以及设置位置的方法内容。
增加棋子位置信息的抽象类为:
<span style="font-size:18px;">
/**
* 需求:棋子的超类,含有一个棋子类别的属性,标志具体的棋子类型
* @author 猛龙过江
*
*/
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+")");
}
}</span>
<span style="font-size:18px;">
/**
* 需求:黑子类
* @author 猛龙过江
*
*/
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();
}
}</span>
<span style="font-size:18px;">
/**
* 需求:白棋子
* @author 猛龙过江
*
*/
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();
}
}</span>
客户端:
<span style="font-size:18px;">import java.util.Random;
/**
* 需求:客户端(测试类)
* @author 猛龙过江
*
*/
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));
}
}
}
}</span>
经过测试后,我们就能够得到带有不同位置的五子棋位置了。我们得到,享元模式的重点在于共享元对象,降低内存的使用空间,提高系统性能。享元对象的外蕴状态是通过客户端来保存传入的,它是可能会发生变化的,因此,在我们进行软件系统设计的时候,一定要区分享元对象的内蕴状态和外蕴状态,不能混淆,更不能互相关联,二者应该是彼此分开的。
6、享元对象的特点:
7、使用场景:
当系统中某个对象类型的实例较多的时候;
当系统设计时候,对象实例真正有区别的分类很少,例如对于拼音,如果对每个字母都new一个对象实例的话,我们需要52个对象,这样实例就太多了,享元模式一般是给出本地内存资源节省的方案,不适于互联网分布式应用的情况;
单例模式本身就是一种享元模式,单例模式中只有一个对象实例,被其他对象所共享。
8、Java中的享元模式
在Java中,lang包下的Integer类,对于经常使用的-128 到 127 范围内的Integer对象当类一被加载时就被创建了,并保存在cache数组中,一旦程序调用valueOf 方法,如果i的值是在-128 到 127 之间就直接在cache缓存数组中去取Integer对象而不是创建一个新对象,这就是享元模式的应用。