11-享元模式
1.享元模式
- 传统的方式实现棋牌游戏,传统的方式可能会消耗较大的内存。
// 棋子类
public class ChessPiece {
private int id;
private String text;
private Color color;
private int positionX;
private int positionY;
public ChessPiece(int id, String text, Color color, int positionX, int positionY) {
this.id = id;
this.text = text;
this.color = color;
this.positionX = positionX;
this.positionY = positionY;
}
public static enum Color {
RED, BLACK;
}
}
// 棋盘类。
// 传统的方式中,每个游戏房间都需要有一个棋盘类,
// 如果有上百万个游戏房间则需要有上百万个棋盘类,保存较多的对象会消耗大量的内存。
public class ChessBoard {
private Map<Integer, ChessPiece> chessPieces = new HashMap<>();
public ChessBoard () {
init();
}
public void init() {
chessPieces.put(1, new ChessPiece(1, "車", ChessPiece.Color.BLACK, 0, 0));
chessPieces.put(2, new ChessPiece(2, "馬", ChessPiece.Color.BLACK, 1, 0));
// ...省略其他棋子的创建。
}
}
- 使用享元模式实现棋盘游戏。
// 享元类
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;
}
}
// 缓存享元类的工厂
public class ChessPieceUnitFactory {
private static final Map<Integer, ChessPieceUnit> pieces = new ConcurrentHashMap<>();
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 chessPieceUnit, int positionX, int positionY) {
this.chessPieceUnit = chessPieceUnit;
this.positionX = positionX;
this.positionY = positionY;
}
}
// 棋盘类。
// 使用享元模式之后,如果有上百万个游戏空间,依然需要上百万个棋盘ChessBoard类和上百万个棋子ChessPiece类。
// 但是棋子ChessPiece类中只有positionX和positionY属性是新的,即没有被共享的,
// 而享元单元ChessPieceUnit类保存的棋子id、文本和颜色是被共享的,不管有多少个游戏房间都只有32个享元单元ChessPieceUnit类
public class ChessBoard {
private Map<Integer, ChessPiece> chessPieces = new HashMap<>();
public ChessBoard () {
init();
}
public void init() {
chessPieces.put(1, new ChessPiece(ChessPieceUnitFactory.getChessPiece(1), 0, 0));
chessPieces.put(2, new ChessPiece(ChessPieceUnitFactory.getChessPiece(2), 1, 0));
// ...省略其他棋子的创建过程。
}
}
2.享元模式总结
- 享元模式通过复用不可变对象,节省内存。如果系统中存在大量的重复的不可变对象,就可以利用享元模式,将对象设计为享元,在内充中只保留一份实例,供多出代码使用。
- 在实际开发中,不仅仅相同的对象可以被设计为享元类,对于相似的对象,可以将这些对象中相同的部分提取出来设计为享元,如棋盘游戏中将棋子的id、文本和颜色抽取出来设计为享元类。
- 享元模式对JVM的垃圾回收不友好,因为工厂类会一直保存对享元类的引用,使享元类即使在不适用的情况下,也不会被垃圾回收。在某些情况下,如果对象的生命周期很短,也不会被密集使用,利用享元模式反而会浪费空间。
- 享元模式和单例模式的区别。在享元模式用,一个类可以创建多个对象,所以享元模式有点类似单例模式的变体:多例模式。但是从设计意图上看,享元模式是为了对象复用,节省内存;多例模式是为了限制对象的个数。
- 享元模式和缓存的区别。常见的缓存,如Redis缓存、数据库缓存,是为了提供访问速度;享元模式是为了对象复用,节省内存。
- 享元模式和对象池、线程池、连接池的区别。对象池、线程池、连接池可以称之为池化技术,池化技术中的复用是为了节省时间(如节省线程、连接的创建时间);享元模式是为了对象复用,节省内存。
3.享元模式在Java中的应用
- 享元模式在Integer中的应用。
// Integer利用享元模式缓存-128~127之间的整数。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
- 享元模式在String中的应用。
// String利用享元模式复用相同的字符串常量。
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // true