一、享元模式的定义
享元(Flyweight)模式的定义:运用共享技术来有効地支持大量细粒度对象的复用。它通过共享已经存在的又橡来大幅度减少需要创建的对象数量,避免大量相似类的开销,从而提高系统资源的利用率。
String常量池、数据库连接池、缓冲池等等都是享元模式的应用,所以说享元模式是池技术的重要实现方式。
二、享元模式优缺点
享元模式的主要优点是:
- 相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。
其主要缺点是:
- 为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
- 读取享元模式的外部状态会使得运行时间稍微变长。
三、享元模式的实现
享元模式中存在以下两种状态:
- 内部状态,即不会随着环境的改变而改变的可共享部分;
- 外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。下面来分析其基本结构和实现方法。
享元模式的主要角色有如下。
- 抽象享元角色(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
- 具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。
- 非享元(Unsharable Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。
- 享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。
如图所示是享元模式的结构图。图中的 UnsharedConcreteFlyweight 是非享元角色,里面包含了非共享的外部状态信息 info;而 Flyweight 是抽象享元角色,里面包含了享元方法 operation(UnsharedConcreteFlyweight state),非享元的外部状态以参数的形式通过该方法传入;ConcreteFlyweight 是具体享元角色,包含了关键字 key,它实现了抽象享元接口;FlyweightFactory 是享元工厂角色,它通过关键字 key 来管理具体享元;客户角色通过享元工厂获取具体享元,并访问具体享元的相关方法。
代码实现如下:
public class FlyweightPattern { public static void main(String[] args) { FlyweightFactory factory=new FlyweightFactory(); Flyweight f01=factory.getFlyweight("a"); Flyweight f02=factory.getFlyweight("a"); Flyweight f03=factory.getFlyweight("a"); Flyweight f11=factory.getFlyweight("b"); Flyweight f12=factory.getFlyweight("b"); f01.operation(new UnsharedConcreteFlyweight("第1次调用a。")); f02.operation(new UnsharedConcreteFlyweight("第2次调用a。")); f03.operation(new UnsharedConcreteFlyweight("第3次调用a。")); f11.operation(new UnsharedConcreteFlyweight("第1次调用b。")); f12.operation(new UnsharedConcreteFlyweight("第2次调用b。")); } } //非享元角色 class UnsharedConcreteFlyweight { private String info; UnsharedConcreteFlyweight(String info) { this.info=info; } public String getInfo() { return info; } public void setInfo(String info) { this.info=info; } } //抽象享元角色 interface Flyweight { public void operation(UnsharedConcreteFlyweight state); } //具体享元角色 class ConcreteFlyweight implements Flyweight { private String key; ConcreteFlyweight(String key) { this.key=key; System.out.println("具体享元"+key+"被创建!"); } public void operation(UnsharedConcreteFlyweight outState) { System.out.print("具体享元"+key+"被调用,"); System.out.println("非享元信息是:"+outState.getInfo()); } } //享元工厂角色 class FlyweightFactory { private HashMap<String, Flyweight> flyweights=new HashMap<String, Flyweight>(); public Flyweight getFlyweight(String key) { Flyweight flyweight=(Flyweight)flyweights.get(key); if(flyweight!=null) { System.out.println("具体享元"+key+"已经存在,被成功获取!"); } else { flyweight=new ConcreteFlyweight(key); flyweights.put(key, flyweight); } return flyweight; } }
测试结果如下:
具体享元a被创建!
具体享元a已经存在,被成功获取!
具体享元a已经存在,被成功获取!
具体享元b被创建!
具体享元b已经存在,被成功获取!
具体享元a被调用,非享元信息是:第1次调用a。
具体享元a被调用,非享元信息是:第2次调用a。
具体享元a被调用,非享元信息是:第3次调用a。
具体享元b被调用,非享元信息是:第1次调用b。
具体享元b被调用,非享元信息是:第2次调用b。
四、享元模式的应用实例
由于享元模式相对不好理解,此处就进行一个五子棋示例:五子棋同围棋一样,包含多个“黑”或“白”颜色的棋子,所以用享元模式比较好。
本实例中的棋子(ChessPieces)类是抽象享元角色,它包含了一个落子的 DownPieces(Graphics g,Point pt) 方法;白子(WhitePieces)和黑子(BlackPieces)类是具体享元角色,它实现了落子方法;Point 是非享元角色,它指定了落子的位置;WeiqiFactory 是享元工厂角色,它通过 ArrayList 来管理棋子,并且提供了获取白子或者黑子的 getChessPieces(String type) 方法;客户类(Chessboard)利用 Graphics 组件在框架窗体中绘制一个棋盘,并实现 mouseClicked(MouseEvent e) 事件处理方法,该方法根据用户的选择从享元工厂中获取白子或者黑子并落在棋盘上。其结构图如图所示:
代码如下:
public class WzqGame { public static void main(String[] args) { new Chessboard(); } } //棋盘 class Chessboard extends MouseAdapter { WeiqiFactory wf; JFrame f; Graphics g; JRadioButton wz; JRadioButton bz; private final int x=50; private final int y=50; private final int w=40; //小方格宽度和高度 private final int rw=400; //棋盘宽度和高度 Chessboard() { wf=new WeiqiFactory(); f=new JFrame("享元模式在五子棋游戏中的应用"); f.setBounds(100,100,500,550); f.setVisible(true); f.setResizable(false); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JPanel SouthJP=new JPanel(); f.add("South",SouthJP); wz=new JRadioButton("白子"); bz=new JRadioButton("黑子",true); ButtonGroup group=new ButtonGroup(); group.add(wz); group.add(bz); SouthJP.add(wz); SouthJP.add(bz); JPanel CenterJP=new JPanel(); CenterJP.setLayout(null); CenterJP.setSize(500, 500); CenterJP.addMouseListener(this); f.add("Center",CenterJP); try { Thread.sleep(500); } catch(InterruptedException e) { e.printStackTrace(); } g=CenterJP.getGraphics(); g.setColor(Color.BLUE); g.drawRect(x, y, rw, rw); for(int i=1;i<10;i++) { //绘制第i条竖直线 g.drawLine(x+(i*w),y,x+(i*w),y+rw); //绘制第i条水平线 g.drawLine(x,y+(i*w),x+rw,y+(i*w)); } } public void mouseClicked(MouseEvent e) { Point pt=new Point(e.getX()-15,e.getY()-15); if(wz.isSelected()) { ChessPieces c1=wf.getChessPieces("w"); c1.DownPieces(g,pt); } else if(bz.isSelected()) { ChessPieces c2=wf.getChessPieces("b"); c2.DownPieces(g,pt); } } } //抽象享元角色:棋子 interface ChessPieces { public void DownPieces(Graphics g,Point pt); //下子 } //具体享元角色:白子 class WhitePieces implements ChessPieces { public void DownPieces(Graphics g,Point pt) { g.setColor(Color.WHITE); g.fillOval(pt.x,pt.y,30,30); } } //具体享元角色:黑子 class BlackPieces implements ChessPieces { public void DownPieces(Graphics g,Point pt) { g.setColor(Color.BLACK); g.fillOval(pt.x,pt.y,30,30); } } //享元工厂角色 class WeiqiFactory { private ArrayList<ChessPieces> qz; public WeiqiFactory() { qz=new ArrayList<ChessPieces>(); ChessPieces w=new WhitePieces(); qz.add(w); ChessPieces b=new BlackPieces(); qz.add(b); } public ChessPieces getChessPieces(String type) { if(type.equalsIgnoreCase("w")) { return (ChessPieces)qz.get(0); } else if(type.equalsIgnoreCase("b")) { return (ChessPieces)qz.get(1); } else { return null; } } }
另外再补充一个在坐标图画圆的例子,代码如下:
interface Shape { void draw(); } class Circle implements Shape { private String color; private int x; private int y; private int radius; public Circle(String color){ this.color = color; } public void setX(int x) { this.x = x; } public void setY(int y) { this.y = y; } public void setRadius(int radius) { this.radius = radius; } @Override public void draw() { System.out.println("Circle: Draw() [Color : " + color +", x : " + x +", y :" + y +", radius :" + radius); } } class ShapeFactory { private static final HashMap<String, Shape> circleMap = new HashMap<String, Shape>(); public static Shape getCircle(String color) { Circle circle = (Circle)circleMap.get(color); if(circle == null) { circle = new Circle(color); circleMap.put(color, circle); System.out.println("Creating circle of color : " + color); } return circle; } } public class DrawShape { private static final String colors[] = { "Red", "Green", "Blue", "White", "Black" }; public static void main(String[] args) { for(int i=0; i < 7; ++i) { Circle circle = (Circle)ShapeFactory.getCircle(getRandomColor()); circle.setX(getRandomX()); circle.setY(getRandomY()); circle.setRadius(100); circle.draw(); } } private static String getRandomColor() { return colors[(int)(Math.random()*colors.length)]; } private static int getRandomX() { return (int)(Math.random()*100 ); } private static int getRandomY() { return (int)(Math.random()*100); } }
测试结果如下:
Creating circle of color : Green Circle: Draw() [Color : Green, x : 25, y :73, radius :100 Creating circle of color : Blue Circle: Draw() [Color : Blue, x : 13, y :37, radius :100 Creating circle of color : Red Circle: Draw() [Color : Red, x : 20, y :91, radius :100 Creating circle of color : White Circle: Draw() [Color : White, x : 29, y :57, radius :100 Circle: Draw() [Color : White, x : 66, y :36, radius :100 Circle: Draw() [Color : Blue, x : 48, y :4, radius :100 Creating circle of color : Black Circle: Draw() [Color : Black, x : 8, y :91, radius :100
五、享元模式的应用场景
前面分析了享元模式的结构与特点,下面分析它适用的应用场景。享元模式是通过减少内存中对象的数量来节省内存空间的,所以以下几种情形适合采用享元模式。
- 系统中存在大量相同或相似的对象,这些对象耗费大量的内存资源。
- 大部分的对象可以按照内部状态进行分组,且可将不同部分外部化,这样每一个组只需保存一个内部状态。
- 由于享元模式需要额外维护一个保存享元的数据结构,所以应当在有足够多的享元实例时才值得使用享元模式。
六、享元模式的扩展
在前面介绍的享元模式中,其结构图通常包含可以共享的部分和不可以共享的部分。在实际使用过程中,有时候会稍加改变,即存在两种特殊的享元模式:单纯享元模式和复合享元模式,下面分别对它们进行简单介绍。
- 单纯享元模式,这种享元模式中的所有的具体享元类都是可以共享的,不存在非共享的具体享元类,其结构图如图所示:
- 复合享元模式,这种享元模式中的有些享元对象是由一些单纯享元对象组合而成的,它们就是复合享元对象。虽然复合享元对象本身不能共享,但它们可以分解成单纯享元对象再被共享,其结构图如图所示: