享元模式

享元模式可以复用对象

享元模式(Flyweight Design Pattern)的意图是复用对象。

当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,
我们就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多处代码引用。
这样可以减少内存中对象的数量,起到节省内存的目的。

实际上,不仅仅相同对象可以设计成享元,对于相似对象,我们也可以将这些对象中相同的部分(字段)提取出来,设计成享元,让这些大量相似对象引用这些享元。

不可变对象 指的是,一旦通过构造函数初始化完成之后,它的状态(对象的成员变量或者属性)就不会再被修改了。之所以要求享元是不可变对象,那是因为它会被多处代码共享使用,避免一处代码对享元进行了修改,影响到其他使用它的代码。

棋牌游戏的棋子可以按照享元模式设计

// 棋子享元类
public class ChessPieceUnit {
  private int id;
  private String text;
  private Color color;

  public ChessPieceUnit(int id, String text, Color color) {
    this.id = id;
    this.text = text;
    this.color = color;
  }

  public static enum Color {
    RED, BLACK
  }

  // ...省略其他属性和getter方法...
}
public class ChessPieceUnitFactory {
  private static final Map<Integer, ChessPieceUnit> pieces = new HashMap<>();

  static {
    pieces.put(1, new ChessPieceUnit(1, "車", ChessPieceUnit.Color.BLACK));
    pieces.put(2, new ChessPieceUnit(2,"馬", ChessPieceUnit.Color.BLACK));
    //...省略摆放其他棋子的代码...
  }

  public static ChessPieceUnit getChessPiece(int chessPieceId) {
    return pieces.get(chessPieceId);
  }
}
// 棋子
public class ChessPiece {
  private ChessPieceUnit chessPieceUnit;
  private int positionX;
  private int positionY;

  public ChessPiece(ChessPieceUnit unit, int positionX, int positionY) {
    this.chessPieceUnit = unit;
    this.positionX = positionX;
    this.positionY = positionY;
  }
  // 省略getter、setter方法
}

// 棋局
public class ChessBoard {
  private Map<Integer, ChessPiece> chessPieces = new HashMap<>();

  public ChessBoard() {
    init();
  }

  private void init() {
    chessPieces.put(1, new ChessPiece(
            ChessPieceUnitFactory.getChessPiece(1), 0,0));
    chessPieces.put(1, new ChessPiece(
            ChessPieceUnitFactory.getChessPiece(2), 1,0));
    //...省略摆放其他棋子的代码...
  }

  public void move(int chessPieceId, int toPositionX, int toPositionY) {
    //...省略...
  }
}

在使用享元模式之前,记录 1 万个棋局,我们要创建 30 万(30*1 万)个棋子的 ChessPieceUnit 对象。
利用享元模式,我们只需要创建 30 个享元对象供所有棋局共享使用即可,大大节省了内存。

Java Integer 对象创建了一个字节的大小范围的享元

IntegerCache 被加载的时候,需要缓存的享元对象会被集中一次性创建好。
缓存对于大部分应用来说最常用的整型值,也就是一个字节的大小(-128 到 127 之间的数据)。

private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;

        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);

        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }

    private IntegerCache() {}
}

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}


Integer i1 = 56;
Integer i2 = 56;
Integer i3 = 129;
Integer i4 = 129;
System.out.println(i1 == i2); // 返回true
System.out.println(i3 == i4); // 返回false

通过 JVM 参数调整缓存大小:

-Djava.lang.Integer.IntegerCache.high=255

-XX:AutoBoxCacheMax=255

Java 字符串常量池中的元素也是享元


String s1 = "aaa";
String s2 = "aaa";
String s3 = new String("aaa");

System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // false

JVM 会专门开辟一块存储区来存储字符串常量,这块存储区叫作 字符串常量池
在某个字符串常量第一次被用到的时候,存储到常量池中,当之后再用到的时候,
直接引用常量池中已经存在的即可,就不需要再重新创建了。

以上代码中在创建 s2 变量时,由于已经存在字符串 aaa 的对象,s2s1指向同一个对象。

享元工厂类一直保存了对享元对象的引用,这就导致享元对象在没有任何代码使用的情况下,
也并不会被 JVM 垃圾回收机制自动回收掉。为了一点点内存的节省而引入一个复杂的设计模式,得不偿失。