享元模式可以复用对象
享元模式(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方法...
}
- 将棋子的
id
、text
、color
属性拆分出来,设计成独立的类,并且作为享元供多个棋盘复用。
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);
}
}
- 利用工厂类来缓存
ChessPieceUnit
信息。通过工厂类获取到的ChessPieceUnit
就是享元。
// 棋子
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) {
//...省略...
}
}
- 所有的
ChessBoard
对象共享这 30 个ChessPieceUnit
对象(因为象棋中只有 30 个棋子)。
在使用享元模式之前,记录 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
的对象,s2
与 s1
指向同一个对象。
享元工厂类一直保存了对享元对象的引用,这就导致享元对象在没有任何代码使用的情况下,
也并不会被 JVM 垃圾回收机制自动回收掉。为了一点点内存的节省而引入一个复杂的设计模式,得不偿失。