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