定义

运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。

所谓“享元”,顾名思义就是被共享的单元。享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象。具体来讲,当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,我们就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,

复用现有的对象,而不是重复创建。

结构

主要角色

  1. 抽象享元角色(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
  2. 具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。
  3. 非享元(Unsharable Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。
  4. 享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。

    结构图

    享元模式(Flyweight Pattern) - 图1

    优点

    节省内存

    相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。

缺点

增加程序的复杂性

为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。而且读取享元模式的外部状态会使得运行时间稍微变长。

不利于JVM垃圾回收

享元模式对 JVM 的垃圾回收并不友好。因为享元工厂类一直保存了对享元对象的引用,这就导致享元对象在没有任何代码使用的情况下,也并不会被 JVM 垃圾回收机制自动回收掉。因此,在某些情况下,如果对象的生命周期很短,也不会被密集使用,利用享元模式反倒可能会浪费更多的内存。

应用

棋盘和棋子

棋子除了坐标外,其他信息是共享的,共享这些信息,而不用为每个棋盘重复创建对象。

  1. // 享元类
  2. public class ChessPieceUnit {
  3. private int id;
  4. private String text;
  5. private Color color;
  6. public ChessPieceUnit(int id, String text, Color color) {
  7. this.id = id;
  8. this.text = text;
  9. this.color = color;
  10. }
  11. public static enum Color {
  12. RED, BLACK
  13. }
  14. // ...省略其他属性和getter方法...
  15. }
  16. public class ChessPieceUnitFactory {
  17. private static final Map<Integer, ChessPieceUnit> pieces = new HashMap<>();
  18. static {
  19. pieces.put(1, new ChessPieceUnit(1, "車", ChessPieceUnit.Color.BLACK));
  20. pieces.put(2, new ChessPieceUnit(2,"馬", ChessPieceUnit.Color.BLACK));
  21. //...省略摆放其他棋子的代码...
  22. }
  23. public static ChessPieceUnit getChessPiece(int chessPieceId) {
  24. return pieces.get(chessPieceId);
  25. }
  26. }
  27. public class ChessPiece {
  28. private ChessPieceUnit chessPieceUnit;
  29. private int positionX;
  30. private int positionY;
  31. public ChessPiece(ChessPieceUnit unit, int positionX, int positionY) {
  32. this.chessPieceUnit = unit;
  33. this.positionX = positionX;
  34. this.positionY = positionY;
  35. }
  36. // 省略getter、setter方法
  37. }
  38. public class ChessBoard {
  39. private Map<Integer, ChessPiece> chessPieces = new HashMap<>();
  40. public ChessBoard() {
  41. init();
  42. }
  43. private void init() {
  44. chessPieces.put(1, new ChessPiece(
  45. ChessPieceUnitFactory.getChessPiece(1), 0,0));
  46. chessPieces.put(1, new ChessPiece(
  47. ChessPieceUnitFactory.getChessPiece(2), 1,0));
  48. //...省略摆放其他棋子的代码...
  49. }
  50. public void move(int chessPieceId, int toPositionX, int toPositionY) {
  51. //...省略...
  52. }
  53. }

文本编辑器中的样式

多个文字共享一个样式,而不是将样式信息塞到每个文字对象中。

  1. public class CharacterStyle {
  2. private Font font;
  3. private int size;
  4. private int colorRGB;
  5. public CharacterStyle(Font font, int size, int colorRGB) {
  6. this.font = font;
  7. this.size = size;
  8. this.colorRGB = colorRGB;
  9. }
  10. @Override
  11. public boolean equals(Object o) {
  12. CharacterStyle otherStyle = (CharacterStyle) o;
  13. return font.equals(otherStyle.font)
  14. && size == otherStyle.size
  15. && colorRGB == otherStyle.colorRGB;
  16. }
  17. }
  18. public class CharacterStyleFactory {
  19. private static final List<CharacterStyle> styles = new ArrayList<>();
  20. public static CharacterStyle getStyle(Font font, int size, int colorRGB) {
  21. CharacterStyle newStyle = new CharacterStyle(font, size, colorRGB);
  22. for (CharacterStyle style : styles) {
  23. if (style.equals(newStyle)) {
  24. return style;
  25. }
  26. }
  27. styles.add(newStyle);
  28. return newStyle;
  29. }
  30. }
  31. public class Character {
  32. private char c;
  33. private CharacterStyle style;
  34. public Character(char c, CharacterStyle style) {
  35. this.c = c;
  36. this.style = style;
  37. }
  38. }
  39. public class Editor {
  40. private List<Character> chars = new ArrayList<>();
  41. public void appendCharacter(char c, Font font, int size, int colorRGB) {
  42. Character character = new Character(c, CharacterStyleFactory.getStyle(font, size, colorRGB));
  43. chars.add(character);
  44. }
  45. }

Integer自动装箱

  1. Integer i1 = 56; // 自动装箱,底层是Integer.valueOf(56),返回一个Integer对象。
  2. Integer i2 = 56;
  3. Integer i3 = 129;
  4. Integer i4 = 129;
  5. System.out.println(i1 == i2); // IntegerCache中缓存了-128 到 127 之间的整型值,此处返回的是同一个对象
  6. System.out.println(i3 == i4); // 自动装箱生成了两个对象,这里是False

一个小扩展:如果分析JVM,发现系统中-128 到 255 之间的数据占用的内存比较多时,可以将缓存最大值调整到255.

  1. //方法一:
  2. -Djava.lang.Integer.IntegerCache.high=255
  3. //方法二:
  4. -XX:AutoBoxCacheMax=255

Long、Short、Byte 等,也都利用了享元模式来缓存 -128 到 127 之间的数据。

字符串常量池

String a=’123’
String b=’123’
String c=new String(‘123’)
其中a,b引用了常量池中的123。

扩展

跟多例的区别

应用享元模式是为了对象复用,节省内存,而应用多例模式是为了限制对象的个数。

享元不是缓存

享元是为了复用已有对象,减少空间占用。而缓存一般是为了提高访问效率。

与对象池、连接池、线程池的区别

享元的复用是为了节省空间。
连接池线程池的复用是重复使用已经建立好的连接或线程,节省时间。