享元模式
“享元”就是被共享的单元。享元模式的使用意图就是复用对象,节省内存,应用的前提是被共享的对象是不可变对象。
具体来说,就是当一个系统中存在大量的重复对象时,如果这些重复的对象时不可变对象,我们就可以利用享元模式,将对象设计成享元,在内存中只保留一份实例,供多处代码引用。这样就可以减少内存中对象的数量,起到节省内存的目的。实际上我们还能抽取相似对象,我们也可以将这些对象中相同的部分提取出来设计成享元,让这些相似的对象去引用享元。
享元模式的简单举例
假设我们现在要开发一个棋牌游戏,比如象棋。在一个游戏大厅有成千上万的虚拟棋局,每个房间对应一个棋盘。每个棋盘要保存每个棋子的信息。利用这些信息就能给用户提供一个完整的棋盘。大致的实现如下:
//棋子
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 positonY){
this.id = id;
this.text = text;
this.color = color;
this.positionX = positionX;
this.positionY = positionY;
}
public static enum Color{
RED,BLACK
}
//....省略其他的属性和方法,以及getter和setter方法.....
}
//棋局
public class ChessBoard{
private Map<Interger , ChessPiece> chessPieces = new HashMap<>();
public ChessBoard(){
init();
}
private void init(){
chessPiece.put(1,new ChessPiece(1,"车",ChessPiece.Color.BLACK,0,0));
//.......省略摆放其他棋子的代码...........
}
private void move(){
//..........省略这部分代码............
}
}
为了记录每个房间当前的棋盘情况,我们需要给每个房间都创建一个ChessBoard类的对象。因为游戏大厅中有成千上万个虚拟的房间,所以保存这么多的对象会消耗大量的内存。这个时候享元模式就派上用场了,对于上述的实现方式,内存会存在大量的相似对象,这些相似的对象的id,text,color都是相同的,只有positionX,positionY是不同的,因此可以将相同的属性抽离出来,成为一个享元,供其他的棋局复用。这样每个棋盘只需要记录每个棋子的位置信息。重构之后的代码如下:
//享元类
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));
//.....省略其他的棋子的代码;
}
public static ChessPieceUnit getChessPiece(int chessPieceId){
return pieces.get(chessPieceId);
}
}
//棋子类
public class ChessPiece{
private ChessPieceUnit chessPieceUnit;
private int positionX;
private int positionY;
public ChessPiece(ChessPiece 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(){
chessPiece.put(1,new ChessPiece(ChessPieceUnitFactory.getChessPieceUnit(1)),0,0);
//....省略其他棋子的代码.....
}
private void move(){
//.....省略代码实现......
}
}
在上面的代码中,我们利用工厂类ChessPieceUnitFactory来缓存ChessPieceUnit享元类的对象。所有的ChessBord类的对象共享这32个ChessPieceUnit享元类对象。如果我们不使用享元模式的话,那么1万个棋局要创建32万个ChessPiece棋子类的对象,但是使用了享元模式,只需要创建32个ChessPieceUnit享元类对象,供所有棋局共享使用,很大程度上节省了内存。
实际上,享元模式对JVM的”垃圾“回收并不友好。因为工厂类一直保存了对享元类的对象的引用,所以,这就导致享元类的对象在没有任何代码使用的情况下,也不会被JVM的"垃圾"回收机制自动回收。在某些情况下,如果对象的生命周期很短,也不会被密集使用,那么利用享元模式反而可能浪费更多的内存。因此,除非通过验证,利用享元模式可以大大的节省内存,否者,就不要过度的使用这个模式。为了这一点点的内存节省而引入一个复杂的设计模式,得不偿失。
享元模式在Java Integer中的使用
在了解享元模式在Integer的使用之前,我们先来理解一下什么是自动装箱(autoboxing)和自动拆箱(unboxing)?以及,Java中是如何判断两个Java对象是否相等(也就是代码中的"=="操作符的含义)?
在Java中,为基本类型提供了对应的包装器类型,如下表
基本数据类型 | 对应的包装器类型 |
---|---|
int | Integer |
long | Long |
float | Float |
double | Double |
boolean | Boolean |
short | Short |
byte | Byte |
char | Character |
自动装箱(autoboxing)是指自动将基本数据类型转化为包装器类型。
自动拆箱(unboxing)是指自动将包装器类型转换为基本数据类型。
例如:
Integer i = 56; //自动装箱
int j = i; //自动拆箱
数值56是基本数据类型(int类型),当赋值给包装类型(Integer类型)的变量时,就会触发自动装箱操作,创建一个Integer对,并且赋值给变量i。实际上“Integer i = 56”这条语句执行了“Integer i = Integer.valueOf(59)”语句。反过来,把包装器类型的变量i赋值给基本数据类型变量j时,就会执行自动拆箱操作,将i中的数据取出来,并赋值给就。“int j = i”这条语句底层执行了"int j = i.intValue()"这条语句。