享元模式(Flyweight Pattern):以共享的方式高效的支援大量的細粒度對象。通過複用記憶體中已存在的對象,降低系統建立對象執行個體的性能消耗。
享元即為共享元對象,在面向對象中,建立對象是很消耗資源的,享元模式就是為避免産生大量的細粒度對象提供解決方案的。
享元模式有兩種狀态,内蘊狀态和外蘊狀态,内蘊狀态存儲于享元對象内部,不會随環境而改變,可以共享;外蘊狀态随外部環境改變,并且由用戶端儲存,不能共享。
享元模式類圖:

抽象享元:為具體享元類規定出公共接口;
具體享元:實作抽象享元類接口,如果有内蘊狀态,需要為内蘊狀态提供存儲空間,這是保證共享的必要條件;
享元工廠:負責建立和管理享元角色,保證享元對象可以被共享,例如用戶端擷取享元對象時,需要判斷該類型對象是否存在,存在則直接傳回,不存在則建立然後傳回;
用戶端:需要維護對所需享元對象的引用,如果有外蘊狀态,需要将外蘊狀态儲存在用戶端;
模式案例
五子棋中會用到很多的黑子和白子,如果對于每一個黑子或白子都建立一個對象會太消耗記憶體。可以使用享元模式,使得在整個遊戲中隻有“黑子”和“白子”兩個對象。
首先建立一個棋子抽象類作為棋子的超類,含有一個棋子辨別的屬性:
/**
* 棋子的超類,含有一個棋子類别的屬性,标志具體的棋子類型
*/
public abstract class AbstractChessman {
//棋子類别
protected String chess;
//構造方法
public AbstractChessman(String chess){
this.chess = chess;
}
//顯示棋子資訊
public void show(){
System.out.println(this.chess);
}
}
黑子類:
/**
* 需求:黑子類
*/
public class BlackChessman extends AbstractChessman {
/*
* 構造方法,初始化黑棋子
*/
public BlackChessman(){
super("●");
System.out.println("--一顆黑棋子誕生了!--");
}
}
白子類:
/**
* 需求:白棋子
*
*/
public class WhiteChessman extends AbstractChessman {
/*
* 構造方法,初始化黑棋子
*/
public WhiteChessman(){
super("○");
System.out.println("--一顆白棋子誕生了!--");
}
}
下面來設計棋子工廠類,棋子工廠類我們設計為單例模式,該類用來生産棋子對象執行個體,并放入緩存當中,下次再獲得棋子對象的時候就從緩存當中獲得。内容如下:
/**
* 需求:棋子工廠,用于生産棋子對象執行個體,并放入緩存中,采用單例模式完成
*/
public class ChessmanFactory {
//單例模式
private static ChessmanFactory chessmanFactory = new ChessmanFactory();
//緩存共享對象
private final Hashtable<Character, AbstractChessman> cache = new Hashtable<Character, AbstractChessman>();
//構造方法私有化
private ChessmanFactory(){
}
//獲得單例工廠對象
public static ChessmanFactory getInstance(){
return chessmanFactory;
}
/*
* 根據字母獲得棋子
*/
public AbstractChessman getChessmanObject(char c){
//從緩存中獲得棋子對象執行個體
AbstractChessman abstractChessman = this.cache.get(c);
//判空
if (abstractChessman==null) {
//說明緩存中沒有該棋子對象執行個體,需要建立
switch (c) {
case 'B':
abstractChessman = new BlackChessman();
break;
case 'W':
abstractChessman = new WhiteChessman();
break;
default:
System.out.println("非法字元,請重新輸入!");
break;
}
//如果有非法字元,那麼對象必定仍為空,是以再進行判斷
if (abstractChessman!=null) {
//放入緩存
this.cache.put(c, abstractChessman);
}
}
//如果緩存中存在棋子對象則直接傳回
return abstractChessman;
}
}
通過用戶端進行測試:
public class Test {
public static void main(String[] args) {
//建立工廠
ChessmanFactory chessmanFactory = ChessmanFactory.getInstance();
//随機數,用于生成棋子對象
Random random = new Random();
int radom = 0;
AbstractChessman abstractChessman = null;
//随機獲得棋子
for (int i = 0; i < 10; i++) {
radom = random.nextInt(2);
switch (radom) {
case 0:
//獲得黑棋子
abstractChessman = chessmanFactory.getChessmanObject('B');
break;
case 1:
//獲得黑棋子
abstractChessman = chessmanFactory.getChessmanObject('W');
break;
}
if (abstractChessman!=null) {
abstractChessman.show();
}
}
}
}
執行後,我們發現“一顆黑棋子誕生了!”和“一顆白棋子誕生了!”各執行了一次,說明在衆多棋子中隻有一個黑棋子和一個白棋子,實作了對象的共享,這就是享元。
外蘊狀态
上邊例子使用到了内蘊狀态,内蘊狀态對于任何一個享元對象來講是完全相同的,可以說,内蘊狀态保證了享元對象能夠被共享。例如上邊的“黑子”和“白子”,代表的狀态就是内蘊狀态。
如果我們為棋子添加位置資訊,因為棋子位置都是不一樣的,是不能夠共享的,這需要使用外蘊狀态。享元對象的外蘊狀态與内蘊狀态是兩類互相獨立的狀态,彼此沒有關聯。
增加棋子位置資訊的抽象類為:
/**
* 需求:棋子的超類,含有一個棋子類别的屬性,标志具體的棋子類型
*/
public abstract class AbstractChessman {
//棋子類别
protected String chess;
//棋子坐标
protected int x;
protected int y;
//構造方法
public AbstractChessman(String chess){
this.chess = chess;
}
//坐标設定
public abstract void point(int x,int y);
//顯示棋子資訊
public void show(){
System.out.println(this.chess+"("+this.x+","+this.y+")");
}
}
完善後的黑子類:
/**
* 需求:黑子類
*/
public class BlackChessman extends AbstractChessman {
/*
* 構造方法,初始化黑棋子
*/
public BlackChessman(){
super("●");
System.out.println("--一顆黑棋子誕生了!--");
}
/*
* 重寫方法
*/
@Override
public void point(int x, int y) {
this.x = x;
this.y = y;
this.show();
}
}
完善後的白子類為:
/**
* 需求:白棋子
*/
public class WhiteChessman extends AbstractChessman {
/*
* 構造方法,初始化黑棋子
*/
public WhiteChessman(){
super("○");
System.out.println("--一顆白棋子誕生了!--");
}
/*
* 重寫方法
*/
@Override
public void point(int x, int y) {
this.x = x;
this.y = y;
this.show();
}
}
在棋子工廠中不需要進行任何改變,因為在棋子工廠中我們獲得的是共享對象,外蘊狀态(位置資訊)是需要在用戶端進行設定的。
用戶端:
public class Test {
public static void main(String[] args) {
//建立工廠
ChessmanFactory chessmanFactory = ChessmanFactory.getInstance();
//随機數,用于生成棋子對象
Random random = new Random();
int radom = 0;
AbstractChessman abstractChessman = null;
//随機獲得棋子
for (int i = 0; i < 10; i++) {
radom = random.nextInt(2);
switch (radom) {
case 0:
//獲得黑棋子
abstractChessman = chessmanFactory.getChessmanObject('B');
break;
case 1:
//獲得黑棋子
abstractChessman = chessmanFactory.getChessmanObject('W');
break;
}
if (abstractChessman!=null) {
abstractChessman.point(i, random.nextInt(15));
}
}
}
}
需要注意的是,為了保證對象被共享,外蘊狀态需要儲存在用戶端,也就是說,x、y值在用戶端儲存。
享元對象特點
享元模式的重點在于“共享對象執行個體,降低記憶體空間”,實作享元模式時,需要規劃共享對象的粒度,才能降低記憶體空間,提高系統性能;例如如果将上邊的x、y轉為内蘊狀态,那麼享元工廠将儲存非常多的對象,失去享元模式的意義。
當系統中某個對象類型的執行個體較多且耗費大量記憶體,并且對象執行個體分類較少,且其部分狀态可以轉為外蘊狀态時,可以使用享元模式。單例模式本身也是一種特殊的享元模式,最大差別是單例模式的類不能被執行個體化,而享元模式類可以被執行個體化。
享元模式的缺點是,為了使對象可以共享,可能會将一些狀态轉為外蘊狀态,使得程式複雜性增加。
Java中的享元模式
Integer類對于經常使用的-128到127範圍内的Integer對象,當類被加載時就被建立了,并儲存在cache數組中,一旦程式調用valueOf方法,如果i的值是在-128到127之間就直接在cache緩存數組中去取Integer對象而不是建立一個新對象,這就是享元模式的應用。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
String類也應用了享元模式,常量池中維護的字元串可以被多個指針引用。
參考:《設計模式那點事》