Java設計模式之享元模式
在說享元模式之前來先看一道題:
public static void main(String[] args) {
Integer i1 = new Integer(50);
Integer i2 = new Integer(50);
System.out.println(i1 == i2);
Integer i3 = new Integer(500);
Integer i4 = new Integer(500);
System.out.println(i3 == i4);
//需要注意下面這種方式存在隱式裝箱
Integer i5 = 50;
Integer i6 = 50;
System.out.println(i5 == i6);
Integer i7 = 500;
Integer i8 = 500;
System.out.println(i7 == i8);
}
很簡單對不對?
答案
false
false
true
false
這便是我想說的享元模式。
享元模式英文為:Flyweight,《JAVA與模式》一書中開頭是這樣描述享元(Flyweight)模式的:
Flyweight在拳擊比賽中指最輕量級,即“蠅量級”或“雨量級”,這里選擇使用“享元模式”的意譯,是因為這樣更能反映模式的用意。享元模式是對象的結構模式。享元模式以共享的方式高效地支持大量的細粒度對象。
享元即為分享元素,字符串常量池、數據庫連接池、緩沖池都是是這個道理。該模式的意圖為:運用共享技術有效地支持大量細粒度的對象。
就像上邊的例子中,Integer類會把較小的數字保存起來,再次新建比較小的Integer對象時會直接返回該對象的引用,從而避免了再次創建對象。通過查看Integer類的源碼我們可以看到Integer會把[-128, 127]之間的數字直接返回共享池中的對象:
public static Integer valueOf(int i) {
//IntegerCache.low = -128
//IntegerCache.high = 127
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
這也是為什么上面例子中第三個輸出true的原因。
單純享元模式
在單純的享元模式中,所有的享元對象都是可以共享的。
單純享元模式所涉及到的角色如下:
- 抽象享元(Flyweight)角色 :父接口,以規定出所有具體享元角色需要實現的方法。
- 具體享元(ConcreteFlyweight)角色:實現抽象享元角色所規定出的接口。
- 享元工廠(FlyweightFactory)角色 :本角色負責創建和管理享元角色。本角色必須保證享元對象可以被系統適當地共享。當一個客戶端對象調用一個享元對象的時候,享元工廠角色會檢查系統中是否已經有一個符合要求的享元對象。如果已經有了,享元工廠角色就應當提供這個已有的享元對象;如果系統中沒有一個適當的享元對象的話,享元工廠角色就應當創建一個合適的享元對象。
上面的Integer例子就是單純的享元模式
到這里可能有同學會問了:這種引用類型的分享對於final的String和Integer來說倒無所謂,因為被final修飾之后不能再改變,所以如何分享引用都沒關系。但是清楚基本類型和引用類型的差別都知道引用類型傳引用之后,改變對象的內部數據會導致對象被修改:
int[] a = {1, 2};
int[] b = a;
b[0] = 20;
System.out.println(a[0]);//同為20
這樣該怎么辦呢?
別擔心,你想到的享元模式也想到了。
符合享元模式
先來認識兩個概念:
內部狀態:在享元對象內部不隨外界環境改變而改變的共享部分。
外部狀態:隨着環境的改變而改變,不能夠共享的狀態就是外部狀態。
由於享元模式區分了內部狀態和外部狀態,所以我們可以通過設置不同的外部狀態使得相同的對象可以具備一些不同的特性,而內部狀態設置為相同部分。在我們的程序設計過程中,我們可能會需要大量的細粒度對象來表示對象,如果這些對象除了幾個參數不同外其他部分都相同,這個時候我們就可以利用享元模式來大大減少應用程序當中的對象。
我們舉一個最簡單的例子,棋牌類游戲大家都有玩過吧,比如說說圍棋和跳棋,它們都有大量的棋子對象,圍棋和五子棋只有黑白兩色,跳棋顏色略多一點,但也是不太變化的,所以棋子顏色就是棋子的內部狀態;而各個棋子之間的差別就是位置的不同,我們落子嘛,落子顏色是定的,但位置是變化的,所以方位坐標就是棋子的外部狀態。
那么為什么這里要用享元模式呢?可以想象一下,上面提到的棋類游戲的例子,比如圍棋,理論上有361個空位可以放棋子,常規情況下每盤棋都有可能有兩三百個棋子對象產生,因為內存空間有限,一台服務器很難支持更多的玩家玩圍棋游戲,如果用享元模式來處理棋子,那么棋子對象就可以減少到只有兩個實例,這樣就很好的解決了對象的開銷問題。
復合享元模式UML:
復合享元角色所涉及到的角色如下:
- 抽象享元(Flyweight)角色 :父接口,以規定出所有具體享元角色需要實現的方法。
- 具體享元(ConcreteFlyweight)角色:實現抽象享元角色所規定出的接口。
- 復合享元(ConcreteCompositeFlyweight)角色 :復合享元角色所代表的對象是不可以共享的,但是一個復合享元對象可以分解成為多個本身是單純享元對象的組合。復合享元角色又稱作不可共享的享元對象。
- 享元工廠(FlyweightFactory)角色 :本角 色負責創建和管理享元角色。本角色必須保證享元對象可以被系統適當地共享。當一個客戶端對象調用一個享元對象的時候,享元工廠角色會檢查系統中是否已經有 一個符合要求的享元對象。如果已經有了,享元工廠角色就應當提供這個已有的享元對象;如果系統中沒有一個適當的享元對象的話,享元工廠角色就應當創建一個 合適的享元對象。
下面就用下棋來舉例:
抽象享元類:
//棋子類
public abstract class AbstractChessman {
// 棋子坐標
protected int x;
protected int y;
// 棋子類別(黑|白)
protected String chess;
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("--BlackChessman Construction Exec!!!");
}
// 點坐標設置
@Override
public void point(int x, int y) {
this.x = x;
this.y = y;
// 顯示棋子內容
show();
}
}
白色棋子:
//白色棋子
public class WhiteChessman extends AbstractChessman {
/**
* 構造方法 初始化白棋子
*/
public WhiteChessman() {
super("○");
System.out.println("--WhiteChessman Construction Exec!!!");
}
// 點坐標設置
@Override
public void point(int x, int y) {
this.x = x;
this.y = y;
// 顯示棋子內容
show();
}
}
享元工廠:
棋子工廠類:
import java.util.Hashtable;
//棋子工廠
public class ChessmanFactory {
// 單例模式工廠
private static ChessmanFactory chessmanFactory = new ChessmanFactory();
// 緩存存放共享對象
private final Hashtable<Character, AbstractChessman> cache = new Hashtable<>();
// 私有化構造方法
private ChessmanFactory() {
}
// 獲得單例工廠
public static ChessmanFactory getInstance() {
return chessmanFactory;
}
/**
* 根據字符獲得棋子
*
* @param c (B:黑棋 W:白棋)
* @return
*/
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:
break;
}
// 為防止 非法字符的進入 返回null
if (abstractChessman != null) {
// 放入緩存
this.cache.put(c, abstractChessman);
}
}
// 如果緩存中存在 棋子對象則直接返回
return abstractChessman;
}
}
客戶端類,即測試類
import java.util.Random;
//測試類
public class Client {
public static void main(String[] args) {
// 創建五子棋工廠
ChessmanFactory fiveChessmanFactory = 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 = fiveChessmanFactory.getChessmanObject('B');
break;
// 獲得白棋
case 1:
abstractChessman = fiveChessmanFactory.getChessmanObject('W');
break;
}
if (abstractChessman != null) {
abstractChessman.point(i, random.nextInt(15));
}
}
}
}
運行結果:
--BlackChessman Construction Exec!!!
●(0,3)
●(1,0)
--WhiteChessman Construction Exec!!!
○(2,1)
●(3,12)
●(4,4)
●(5,9)
●(6,9)
○(7,2)
●(8,11)
○(9,6)
ps:復合享元不可共享,只需繼承抽象享元即可,不再演示。
小結
享元模式:
- 意圖:運用共享技術有效地支持大量細粒度的對象。
- 主要解決:在有大量對象時,有可能會造成內存溢出,我們把其中共同的部分抽象出來,如果有相同的業務請求,直接返回在內存中已有的對象,避免重新創建。
- 何時使用:
- 系統中有大量對象。
- 這些對象消耗大量內存。
- 這些對象的狀態大部分可以外部化。
- 這些對象可以按照內蘊狀態分為很多組,當把外蘊對象從對象中剔除出來時,每一組對象都可以用一個對象來代替。
- 系統不依賴於這些對象身份,這些對象是不可分辨的。
- 如何解決:用唯一標識碼判斷,如果在內存中有,則返回這個唯一標識碼所標識的對象。
- 關鍵代碼:用 HashMap 存儲這些對象。
- 應用實例:
- Integer類,在[-128, 127]大小范圍內將直接返回相同的對象
- JAVA 中的 String,如果有則返回,如果沒有則創建一個字符串保存在字符串緩存池里面。
- 數據庫的數據池。
- 優點:大大減少對象的創建,降低系統的內存,使效率提高。
- 缺點:提高了系統的復雜度,需要分離出外部狀態和內部狀態,而且外部狀態具有固有化的性質,不應該隨着內部狀態的變化而變化,否則會造成系統的混亂。
- 使用場景:
- 系統有大量相似對象。
- 需要緩沖池的場景。
- 注意事項:
- 注意划分外部狀態和內部狀態,否則可能會引起線程安全問題。
- 這些類必須有一個工廠對象加以控制