JAVA设计模式-14-享元模式

参考:http://blog.****.net/pengjunlee/article/details/52770961

一、什么是享元模式
享元Flyweight模式是构造型模式之一,它通过共享数据使得相同对象在内存中仅创建一个实例,以降低系统创建对象实例的性能消耗。
以博客里写文章为例来说吧,把文章和文章中的文字看作对象,我们每在博客里写一个字就相当于是创建一个文字对象,假如我们写的文章有一万个字,那我们岂不是要创建一万次文字对象,如此频繁的创建对象势必会严重拖累系统的性能。
享元模式通过数据共享使得重复使用的相同对象在内存中仅创建一次:这就好比汉字中的 “的” 字虽然在文章中反复出现,但它仅在第一次被使用的时候创建 “的” (共享对象)并保存起来,之后再用到 “的” 字的话直接获取之前创建好的 “的” 对象即可,不需要再次创建了。
下面是两张使用享元模式前后的对比图,看完这两张图相信大家就能理解享元模式的作用了。
使用享元模式前:
JAVA设计模式-14-享元模式

使用享元模式后:

JAVA设计模式-14-享元模式

享元对象能做到共享的关键是区分内蕴状态和外蕴状态:
1:内蕴状态(intrinsic State)    内蕴状态指存储在享元对象内部、不会随环境改变而改变的状态,内蕴状态可以被共享。
2:外蕴状态(extrinsic State)    外蕴状态指随环境改变而改变的、不可以共享的状态。享元对象的外蕴状态必须由客户端保存,并在享元对象被创建之后,在需要使用的时候再传入到享元对象内部。外蕴状态与内蕴状态是相互独立的。
在上例中,文字的内蕴状态包括:文字的字体、大小、颜色等,文字的外蕴状态包括:文字在文章中出现的位置等。
二、享元模式在Java中的典型应用
在Java语言中,String类型就使用了享元模式。String对象是final类型,对象一旦创建就不可改变,同时JAVA会确保一个字符串常量(例如:"Flyweight")在常量池中只能存在一份拷贝,例如下面这段简单代码:
[java] view plain copy
  1. String str1="Flyweight";  
  2. String str2="Flyweight";  
  3.   
  4. if(str1==str2){  
  5.  System.out.println("str1与str2指向同一对象");  
  6. }  
  7. else  
  8. {  
  9.  System.out.println("str1与str2指向不同对象");  
  10. }  
打印结果:
[plain] view plain copy
  1. str1与str2指向同一对象  
在以上代码中if语句比较的是对象str1和str2中存储的内存地址是否相同,从最终的打印结果可以看出:两次创建的字符串常量"Flyweight"内存地址相同,即两个字符串其实是同一个对象。
三、享元模式的结构

享元模式分为单纯享元模式和复合享元模式两种。

单纯享元模式:

JAVA设计模式-14-享元模式

单纯享元模式涉及的角色及其职责如下:

抽象享元(Flyweight)角色:所有具体享元类的父类或实现的接口,以规定所有具体享元角色需要实现的方法。

具体享元(ConcreteFlyweight)角色:又叫单纯享元角色,是抽象享元角色的具体实现类,如果有内蕴状态的话,它负责为内蕴状态提供存储空间。具体享元对象的内蕴状态必须与对象所处的周围环境无关,从而使得享元对象可以在系统内共享。

享元工厂(FlyweightFactory)角色:负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当地共享。当客户端调用某个享元对象的时候,享元工厂角色会检查系统中是否已经存在符合要求的享元对象,如果已经存在则直接提供,若不存在则新创建一个。

单纯享元模式结构示意源代码如下:

在单纯的享元模式中,Flyweight接口用以规定出所有具体享元角色需要实现的方法。

[java] view plain copy
  1. public interface Flyweight {  
  2.   
  3.     /** 
  4.      * 用以规定所有具体享元角色需要实现的方法. 
  5.      */  
  6.     void operation(String extrinsicState);  
  7.   
  8. }  

具体享元ConcreteFlyweight一方面要保存需共享的内蕴状态,另一方面将外蕴状态作为参数传入operation()方法中,可改变方法的行为,但并不改变对象的内蕴状态。

[java] view plain copy
  1. public class ConcreteFlyweight implements Flyweight {  
  2.   
  3.     private String intrinsicState = null;  
  4.   
  5.     /** 
  6.      * 构造函数中内蕴状态作为参数传入. 
  7.      */  
  8.     public ConcreteFlyweight(String intrinsicState) {  
  9.         this.intrinsicState = intrinsicState;  
  10.     }  
  11.   
  12.     /** 
  13.      * 外蕴状态作为参数传入方法中,可改变方法的行为,但并不改变对象的内蕴状态. 
  14.      */  
  15.     @Override  
  16.     public void operation(String extrinsicState) {  
  17.         System.out.println();  
  18.         System.out.println("内蕴状态:" + intrinsicState);  
  19.         System.out.println("外蕴状态:" + extrinsicState);  
  20.         System.out.println("-------------------------");  
  21.     }  
  22.   
  23.     public String toString() {  
  24.         return this.intrinsicState;  
  25.     }  
  26. }  
享元工厂FlyweightFactory负责维护一个享元对象存储池(Flyweight Pool)来存放内部状态的对象。为了调用方便,该工厂类一般使用单例模式来实现。

[java] view plain copy
  1. import java.util.HashMap;  
  2. import java.util.Map;  
  3.   
  4. public class FlyweightFactory {  
  5.   
  6.     // 用来登记和存储已经创建过的享元对象  
  7.     private Map<String, Flyweight> flyweightMap = new HashMap<String, Flyweight>();  
  8.   
  9.     // 采用单例模式  
  10.     private static FlyweightFactory flyweightFactory = new FlyweightFactory();  
  11.   
  12.     // 私有化享元工厂的构造方法  
  13.     private FlyweightFactory() {  
  14.     }  
  15.   
  16.     // 获取单例享元工厂的实例  
  17.     public static FlyweightFactory getFlyweightFactory() {  
  18.         return flyweightFactory;  
  19.     }  
  20.   
  21.     public Flyweight getFlyweight(String intrinsicState) {  
  22.         Flyweight flyweight = flyweightMap.get(intrinsicState);  
  23.         if (null == flyweight) {  
  24.             flyweight = new ConcreteFlyweight(intrinsicState);  
  25.             flyweightMap.put(intrinsicState, flyweight);  
  26.         }  
  27.         return flyweight;  
  28.     }  
  29.   
  30.     public Map<String, Flyweight> getFlyweightMap() {  
  31.         return flyweightMap;  
  32.     }  
  33.   
  34. }  

在Client类中创建的main()方法中进行测试。

[java] view plain copy
  1. import java.util.Map.Entry;  
  2.   
  3. public class Client {  
  4.   
  5.     public static void main(String[] args) {  
  6.         FlyweightFactory flyweightFactory = FlyweightFactory.getFlyweightFactory();  
  7.         flyweightFactory.getFlyweight("爱").operation("位置1");  
  8.         flyweightFactory.getFlyweight("我").operation("位置2");  
  9.         flyweightFactory.getFlyweight("的").operation("位置3");  
  10.         flyweightFactory.getFlyweight("人").operation("位置4");  
  11.         flyweightFactory.getFlyweight("和").operation("位置5");  
  12.         flyweightFactory.getFlyweight("我").operation("位置6");  
  13.         flyweightFactory.getFlyweight("爱").operation("位置7");  
  14.         flyweightFactory.getFlyweight("的").operation("位置8");  
  15.         flyweightFactory.getFlyweight("人").operation("位置9");  
  16.           
  17.         System.out.println("已存储的享元对象个数:"+flyweightFactory.getFlyweightMap().size()+"个");  
  18.         for (Entry<String, Flyweight> entry : flyweightFactory.getFlyweightMap().entrySet()) {  
  19.             System.out.println(entry.getValue());  
  20.         }  
  21.   
  22.     }  
  23.   
  24. }  
运行程序打印结果如下:

[plain] view plain copy
  1. 内蕴状态:爱  
  2. 外蕴状态:位置1  
  3. -------------------------  
  4.   
  5. 内蕴状态:我  
  6. 外蕴状态:位置2  
  7. -------------------------  
  8.   
  9. 内蕴状态:的  
  10. 外蕴状态:位置3  
  11. -------------------------  
  12.   
  13. 内蕴状态:人  
  14. 外蕴状态:位置4  
  15. -------------------------  
  16.   
  17. 内蕴状态:和  
  18. 外蕴状态:位置5  
  19. -------------------------  
  20.   
  21. 内蕴状态:我  
  22. 外蕴状态:位置6  
  23. -------------------------  
  24.   
  25. 内蕴状态:爱  
  26. 外蕴状态:位置7  
  27. -------------------------  
  28.   
  29. 内蕴状态:的  
  30. 外蕴状态:位置8  
  31. -------------------------  
  32.   
  33. 内蕴状态:人  
  34. 外蕴状态:位置9  
  35. -------------------------  
  36. 已存储的享元对象个数:5个  
  37. 爱  
  38. 我  
  39. 的  
  40. 人  
  41. 和  
从以上程序的运行结果不难看出,虽然我们新建了9个Flyweight对象,但在内存池中实际上只有5个,这就是共享的含义。
复合享元模式:

在单纯享元模式中,所有的享元对象都是单纯享元对象,也就是说都是可以直接共享的。还有一种较为复杂的情况,将一些单纯享元对象使用合成模式加以组合,形成复合享元对象。这样的复合享元对象本身不能共享,但是它们可以分解成单纯享元对象,而后者则可以共享。

JAVA设计模式-14-享元模式

与单纯享元模式相比,复合享元模式增加了一个复合享元(UnsharedConcreteFlyweight)角色,复合享元角色所代表的对象是不可以共享的,又称作不可共享的享元对象。

复合享元角色实现了抽象享元角色所规定的接口,也就是operation()方法,这个方法有一个参数,代表复合享元对象的外蕴状态。一个复合享元对象的所有单纯享元对象的外蕴状态都是与复合享元对象的外蕴状态相等的;而一个复合享元对象所含有的单纯享元对象的内蕴状态一般是不相等的,不然就没有使用价值了。

复合享元模式本质上就相当于将原本掺杂在一起的单纯享元对象,根据不同的外蕴状态重新进行了二次分组。这就好比在饭店点餐时,饭店会将不同顾客点的菜记录在顾客自己的点菜单(账单)上一样,与将所有顾客点的菜都记录到一个点菜单上相比,分开记录很明显更便于管理、一目了然,接下来我们就以点餐为例来学习复合享元模式。

复合享元模式与单纯享元模式的差异代码如下:

首先复合享元模式增加了一个不可共享的复合享元(UnsharedConcreteFlyweight)角色。

[java] view plain copy
  1. import java.util.HashMap;  
  2. import java.util.Map;  
  3.   
  4. public class UnsharedConcreteFlyweight implements Flyweight {  
  5.       
  6.     //用来登记和存储已经创建过的享元对象  
  7.     private Map<String, Flyweight> flyweightMap = new HashMap<String, Flyweight>();  
  8.       
  9.     //指定所有享元对象的外蕴状态  
  10.     private String extrinsicState = null;  
  11.   
  12.     // 增加一个新的单纯享元对象到集合中  
  13.     public void add(String intrinsicState, Flyweight flyweight) {  
  14.         flyweightMap.put(intrinsicState, flyweight);  
  15.     }  
  16.   
  17.     @Override  
  18.     public void operation(String extrinsicState) {  
  19.         this.extrinsicState = extrinsicState;  
  20.     }  
  21.   
  22.     public void getFlyweight(String intrinsicState){  
  23.         Flyweight flyweight=flyweightMap.get(intrinsicState);  
  24.         if(null!=flyweight){  
  25.             flyweight.operation(extrinsicState);  
  26.         }  
  27.         else{  
  28.             System.out.println();  
  29.             System.out.println(extrinsicState+"未点过:"+intrinsicState);  
  30.             System.out.println("-------------------------");  
  31.         }  
  32.     }  
  33.   
  34.     public Map<String, Flyweight> getFlyweightMap() {  
  35.         return flyweightMap;  
  36.     }  
  37.       
  38. }  

享元工厂FlyweightFactory仅需添加一个用于获取复合享元对象的getUnsharedFlyweight()方法,其他代码无需修改。

[java] view plain copy
  1. public class FlyweightFactory {  
  2.   
  3.     /** 
  4.      * 此处省略单纯享元模式中享元工厂已有代码 
  5.      * ... 
  6.      * ... 
  7.      * 原有代码不变,仅新增一个getUnsharedFlyweight()方法. 
  8.      */  
  9.   
  10.     public UnsharedConcreteFlyweight getUnsharedFlyweight(List<String> intrinsicStates) {  
  11.         UnsharedConcreteFlyweight unsharedFlyweight = new UnsharedConcreteFlyweight();  
  12.         for (String intrinsicState : intrinsicStates) {  
  13.             unsharedFlyweight.add(intrinsicState, getFlyweight(intrinsicState));  
  14.         }  
  15.         return unsharedFlyweight;  
  16.     }  
  17.   
  18. }  

在Client客户端类进行测试,其修改后代码如下。

[java] view plain copy
  1. import java.util.ArrayList;  
  2. import java.util.List;  
  3.   
  4. public class Client {  
  5.   
  6.     public static void main(String[] args) {  
  7.     List<String> intrinsicStates = new ArrayList<String>();  
  8.         intrinsicStates.add("木须肉");  
  9.         intrinsicStates.add("地三鲜");  
  10.         intrinsicStates.add("孜然牛肉");  
  11.           
  12.         FlyweightFactory flyFactory =FlyweightFactory.getFlyweightFactory();  
  13.           
  14.         UnsharedConcreteFlyweight unsharedFlyweight1 = flyFactory.getUnsharedFlyweight(intrinsicStates);  
  15.         UnsharedConcreteFlyweight unsharedFlyweight2 = flyFactory.getUnsharedFlyweight(intrinsicStates);  
  16.           
  17.         System.out.println("复合享元对象是否可以共享:" + (unsharedFlyweight1 == unsharedFlyweight2));  
  18.          
  19.         unsharedFlyweight1.operation("一号桌");  
  20.         unsharedFlyweight2.operation("五号桌");  
  21.           
  22.         //开始给一号桌上菜了  
  23.         unsharedFlyweight1.getFlyweight("木须肉");  
  24.         unsharedFlyweight1.getFlyweight("地三鲜");  
  25.         unsharedFlyweight1.getFlyweight("孜然牛肉");  
  26.         unsharedFlyweight1.getFlyweight("霸王餐");  
  27.           
  28.         System.out.println();  
  29.           
  30.         String intrinsicState ="木须肉";  
  31.         Flyweight flyweight1 = unsharedFlyweight1.getFlyweightMap().get(intrinsicState);  
  32.         Flyweight flyweight2 = unsharedFlyweight2.getFlyweightMap().get(intrinsicState);  
  33.         System.out.println("不同复合享元对象中的单纯享元对象是否可以共享:" + (flyweight1 == flyweight2));  
  34.     }  
  35.   
  36. }  
运行程序打印结果如下:

[plain] view plain copy
  1. 复合享元对象是否可以共享:false  
  2.   
  3. 内蕴状态:木须肉  
  4. 外蕴状态:一号桌  
  5. -------------------------  
  6.   
  7. 内蕴状态:地三鲜  
  8. 外蕴状态:一号桌  
  9. -------------------------  
  10.   
  11. 内蕴状态:孜然牛肉  
  12. 外蕴状态:一号桌  
  13. -------------------------  
  14.   
  15. 一号桌未点过:霸王餐  
  16. -------------------------  
  17.   
  18. 不同复合享元对象中的单纯享元对象是否可以共享:true  
四、享元模式应用举例
场景:如下图所示,我们要做一个画图程序,为简单起见,假设该画图程序仅能画相同大小(直径20mm)的圆,但圆的颜色和位置可以不同。
JAVA设计模式-14-享元模式

如果想要使用享元模式来对该画图程序进行设计的话,首先要提取出享元对象(圆)的内蕴状态和外蕴状态。

内蕴状态:圆的颜色。

外蕴状态:圆的位置。

接下来,首先定义享元对象的抽象接口Circle,只含有一个draw()方法。
[java] view plain copy
  1. public interface Circle {  
  2.   
  3.     //画圆方法,需传入绘制的坐标x,y  
  4.     void draw(int x,int y);  
  5. }  
然后再定义具体享元角色类ConcreteCircle。
[java] view plain copy
  1. import java.awt.Color;  
  2.   
  3. public class ConcreteCircle implements Circle {  
  4.       
  5.     //内蕴状态,圆的颜色  
  6.     private Color color;  
  7.       
  8.     //构造函数中内蕴状态作为参数传入.  
  9.     public ConcreteCircle(Color color){  
  10.         super();  
  11.         this.color=color;  
  12.     }  
  13.   
  14.     // 外蕴状态作为参数传入绘图方法中,用以指定绘图位置.  
  15.     @Override  
  16.     public void draw(int x, int y) {  
  17.         System.out.println("圆的颜色:"+this.color+",坐标:X "+x+",Y "+y+"。");  
  18.     }  
  19.   
  20. }  
再定义享元工厂类CircleFactory。
[java] view plain copy
  1. import java.awt.Color;  
  2. import java.util.HashMap;  
  3. import java.util.Map;  
  4.   
  5. public class CircleFactory {  
  6.   
  7.     private Map<Color, Circle> circleMap = new HashMap<Color, Circle>();  
  8.   
  9.     // 采用单例模式  
  10.     private static CircleFactory circleFactory = new CircleFactory();  
  11.   
  12.     // 私有化享元工厂的构造方法  
  13.     private CircleFactory() {  
  14.     }  
  15.   
  16.     // 获取单例享元工厂的实例  
  17.     public static CircleFactory getCircleFactory() {  
  18.         return circleFactory;  
  19.     }  
  20.   
  21.     public Circle getCircle(Color color) {  
  22.         Circle circle = circleMap.get(color);  
  23.         if (null == circle) {  
  24.             circle = new ConcreteCircle(color);  
  25.             circleMap.put(color, circle);  
  26.         }  
  27.         return circle;  
  28.     }  
  29. }  
最后创建客户端Client类。
[java] view plain copy
  1. import java.awt.Color;  
  2.   
  3. public class Client {  
  4.       
  5.     public static void main(String[] args) {  
  6.         /** 
  7.          *  7个红色圆的坐标:(0,0) (0,25) (0,50) (0,75) (0,100) (25,100) (50,100) 
  8.          * 11个蓝色圆的坐标:(100,0) (125,0) (150,0) (100,25) (100,50) (125,50) (150,50) (100,75) (100,100) (125,100) (150,100) 
  9.          * 11个绿色圆的坐标:(200,0) (225,0) (250,0) (200,25) (200,50) (225,50) (250,50) (200,75) (200,100) (225,100) (250,100) 
  10.          */  
  11.         CircleFactory circleFactory=CircleFactory.getCircleFactory();  
  12.           
  13.         circleFactory.getCircle(Color.RED).draw(00);  
  14.         circleFactory.getCircle(Color.RED).draw(025);  
  15.         circleFactory.getCircle(Color.RED).draw(050);  
  16.         circleFactory.getCircle(Color.RED).draw(075);  
  17.         circleFactory.getCircle(Color.RED).draw(0100);  
  18.         circleFactory.getCircle(Color.RED).draw(25100);  
  19.         circleFactory.getCircle(Color.RED).draw(50100);  
  20.         /** 
  21.          * ...篇幅所限,蓝色圆和绿色圆的绘制代码此处省略... 
  22.          */  
  23.     }  
  24. }  
运行程序打印结果如下:
[java] view plain copy
  1. 圆的颜色:java.awt.Color[r=255,g=0,b=0],坐标:X 0,Y 0。  
  2. 圆的颜色:java.awt.Color[r=255,g=0,b=0],坐标:X 0,Y 25。  
  3. 圆的颜色:java.awt.Color[r=255,g=0,b=0],坐标:X 0,Y 50。  
  4. 圆的颜色:java.awt.Color[r=255,g=0,b=0],坐标:X 0,Y 75。  
  5. 圆的颜色:java.awt.Color[r=255,g=0,b=0],坐标:X 0,Y 100。  
  6. 圆的颜色:java.awt.Color[r=255,g=0,b=0],坐标:X 25,Y 100。  
  7. 圆的颜色:java.awt.Color[r=255,g=0,b=0],坐标:X 50,Y 100。  
五、享元模式的适用性

Flyweight模式的有效性很大程度上取决于如何使用它以及在何处使用它。当出现下列情形时可以考虑使用Flyweight模式。

1)一个应用程序使用了大量的对象。
2)完全由于使用大量的对象,造成很大的存储开销。
3)对象的大多数状态都可变为外部状态。
4)如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象。
5)应用程序不依赖对象标识。

六、享元模式的特点
        享元模式的优点:减少对象数量,节省内存空间。

        享元模式的缺点

  1)享元模式使得系统更加复杂。为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化。
  2)享元模式将享元对象的状态外部化,而读取外部状态使得运行时间稍微变长。

 享元模式的本质分离与共享

七、总结
享元模式是一个考虑系统性能的设计模式,通过使用享元模式可以节约内存空间,提高系统的性能。享元模式的核心在于享元工厂类,享元工厂类的作用在于提供一个用于存储享元对象的享元池,用户需要对象时,首先从享元池中获取,如果享 元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象。