1. 程式人生 > 實用技巧 >設計模式——享元模式

設計模式——享元模式

享元模式(Flyweight Pattern):主要用於減少建立物件的數量,以減少記憶體佔用和提高效能。在面向物件程式的設計過程中,有時需要建立大量相似的物件例項。如果都建立將會消耗很多系統資源,它是系統性能提高的一個瓶頸。但如果將這些物件的相似部分抽取出來共享,則能節約大量的系統資源,這就是享元模式的產生背景。在 Java 中 String 值的儲存就使用了享元模式,相同的值只存一個。

一、基本介紹


1、享元模式(Flyweight Pattern)也叫 “蠅量模式”:運用共享技術有效地支援大量細粒度物件。
2、常用於系統底層開發,解決系統的效能問題。像資料庫連線池,裡面都是建立好的連線物件,在這些連線物件中有我們需要的則直接拿過來用,避免重新建立,如果沒有我們需要的,則建立一個。
3、享元模式能夠解決重複物件的記憶體消耗問題,當系統中有大量相似物件,需要緩衝池時。不需要總建立新物件,可以從緩衝池裡拿。這樣可以降低系統記憶體,同時提高效率。
4、享元模式經典的應用的場景就是池技術,String 常量池、資料庫連線池、緩衝池等等都是享元模式的應用,享元模式是池技術的重要實現方式。

二、享元模式的特點


享元模式的主要優點:相同物件只要儲存一份,降低了系統中物件的數量,從而降低了系統中細粒度物件給記憶體帶來的壓力。
享元模式的主要缺點:為了使物件共享,需要將一些不能共享的狀態外部化,這將增加程式的複雜性。需要分離出外部狀態和內部狀態,而且外部狀態具有固有化性質,不應該隨著內部狀態的變化而變化,否則會造成系統的混亂。
享元模式的主要意圖:執行享元模式有效地支援大量細粒度物件。
享元模式主要解決的問題:在有大量相似物件時,有可能會造成記憶體溢位,我們把其中共同的部分抽取出來,如果有相同的業務請求,直接返回記憶體中已有的物件,避免重新建立。
享元模式如何解決問題:用唯一標識碼判斷,如果記憶體中有,則返回唯一標識所標識的物件。
享元模式關鍵程式碼

:用 HashMap 儲存物件,key 表示唯一標識,value 為共享物件。
享元模式使用場景:1)、系統有大量相似物件。2)、需要緩衝池的場景。
享元模式注意事項:1)、注意劃分外部狀態和內部狀態,否則可能會引起執行緒安全問題。2)、這些類必須有一個工廠物件加以控制。

三、內部狀態和外部狀態


1)、享元模式提出了兩個要求:細粒度和共享物件。這裡就涉及到內部狀態和外部狀態,既將物件的資訊分為兩部分:內部狀態和外部狀態。
2)、內部狀態:指物件共享出來的資訊,儲存在享元物件內部且不會隨環境的改變而改變。
3)、外部狀態:指物件得以依賴的一個標記,是隨環境改變而改變的,不可共享的狀態。

四、享元模式結構類圖


享元模式的主要角色如下:
1)、抽象享元角色(Flyweight):是所有的具體享元類的基類,為具體享元規範需要實現的公共介面,非享元的外部狀態以引數的形式通過方法傳入。
2)、具體享元(Concrete Flyweight)角色:實現抽象享元角色中所規定的介面。
3)、非享元(Unsharalbe Flyweight)角色:是不可以共享的外部狀態,它以引數的形式注入具有享元的相關方法中。
4)、享元工廠(Flyweight Factory)角色:負責建立和管理享元角色。當客戶物件請求一個享元物件時,享元工廠檢查系統中是否存在符合要求的享元物件,如果存在則提供給客戶;如果不存在,則建立一個新的享元物件。

五、享元模式案例分析


享元模式在五子棋中的應用:包含多個內部狀態 “黑” 和 “白” 顏色的棋子和外部狀態 棋子的座標 ,所以適合享元模式、

【1】抽象享元角色:棋子(ChessPieces)類包含了一個落子的方法:downPieces(Point pt)

1 public interface ChessPieces {
2     //落子方法 color:內部狀態  pt:外部狀態
3     public void downPieces(Point pt);
4 }

【2】具體享元角色:抽象享元角色的實現類(黑子/白子 的實現類)

☛ 黑子 實現類如下:

1 public class BlackPieces implements ChessPieces{
2     @Override
3     public void downPieces(Point pt) {
4         System.out.println("當前獲取到的為===黑===顏色的棋子");
5         System.out.println("座標X="+pt.getX()+";Y="+pt.getY());
6     }
7 }

☞ 白子 實現類如下:

1 public class WhitePieces implements ChessPieces{
2     @Override
3     public void downPieces(Point pt) {
4         System.out.println("當前獲取到的為===白===顏色的棋子");
5         System.out.println("座標X="+pt.getX()+";Y="+pt.getY());
6     }
7 }

【3】享元工廠角色:通過內部狀態,將物件進行分類儲存,相同的內部狀態只存一個物件即可。

 1 public class PiecesFactory {
 2     //儲存已建立的棋子   享元模式的精華
 3     HashMap<String, ChessPieces> pieces = new HashMap<>();
 4     private final String WRITE = "Write";
 5     private final String BLACK = "Black";
 6     //建立一個靜態方法 獲取棋子物件
 7     public ChessPieces getPieceInstance(String color) {
 8         if(pieces.get(color) == null) {
 9             if(color == WRITE) {
10                 WhitePieces whitePieces = new WhitePieces();
11                 pieces.put(color, whitePieces);
12             }else if(color == BLACK){
13                 BlackPieces blackPieces = new BlackPieces();
14                 pieces.put(color, blackPieces);
15             }else {
16                 System.out.println("不存在的顏色");
17                 return null;
18             }
19         }
20         return pieces.get(color);
21     }
22     
23     //檢視 hashmap 中總計的例項數量
24     public int getInstallCount() {
25         return pieces.size();
26     }
27 }

【4】客戶端應用:將內部狀態傳遞給工廠類,外部狀態傳遞給具體實現類。

 1 public class Clinet {
 2     private final static String WRITE = "Write";
 3     private final static String BLACK = "Black";
 4     
 5     public static void main(String[] args) {
 6         //建立工程
 7         PiecesFactory factory = new PiecesFactory();
 8         //獲取白色棋子
 9         //下琪1 = 白
10         ChessPieces piece1 = factory.getPieceInstance(WRITE);
11         piece1.downPieces(new Point(1,2));
12         //下琪1 = 黑
13         ChessPieces pieceB1 = factory.getPieceInstance(BLACK);
14         pieceB1.downPieces(new Point(2,2));
15         //下琪2 = 白
16         ChessPieces piece2 = factory.getPieceInstance(WRITE);
17         piece2.downPieces(new Point(2, 3));
18         //下琪2 = 黑
19         ChessPieces pieceB2 = factory.getPieceInstance(BLACK);
20         pieceB2.downPieces(new Point(3,2));
21         //下琪3 = 白
22         ChessPieces piece3 = factory.getPieceInstance(WRITE);
23         piece3.downPieces(new Point(5, 7));
24         //下琪3 = 黑
25         ChessPieces pieceB3 = factory.getPieceInstance(BLACK);
26         pieceB3.downPieces(new Point(6,6));
27         
28         /**
29          * 結果:
30          * 當前獲取到的為===白===顏色的棋子
31          * 座標X=1;Y=2
32          * 當前獲取到的為===黑===顏色的棋子
33          * 座標X=2;Y=2
34          * 當前獲取到的為===白===顏色的棋子
35          * 座標X=2;Y=3
36          * 當前獲取到的為===黑===顏色的棋子
37          * 座標X=3;Y=2
38          * 當前獲取到的為===白===顏色的棋子
39          * 座標X=5;Y=7
40          * 當前獲取到的為===黑===顏色的棋子
41          * 座標X=6;Y=6
42          */
43         
44         //重點是,這6顆棋子 總共建立了多少個物件
45         System.out.println(factory.getInstallCount());
46         /**
47          * 輸入結果:2   複合享元模式的應用
48          */
49     }
50 }

六、享元模式 JDK-Interger 應用原始碼分析


【1】我們在建立 Interger 物件時,有兩種方式:分別是 valueOf() 和 new 的形式,如下:我們會發現 valueOf() 建立的例項是相等的,說明使用了享元模式,下面我們就檢視下其原始碼:

 1 public static void main(String[] args) {
 2         Integer x = Integer.valueOf(127); // 得到 x例項,型別 Integer
 3         Integer y = new Integer(127); // 得到 y 例項,型別 Integer
 4         Integer z = Integer.valueOf(127);//..
 5         Integer w = new Integer(127);
 6                 
 7         //我們會發現valueOf建立的例項是相等的,說明使用了享元模式。new 每次給建立一個新的物件
 8         System.out.println(x == z ); // true
 9         System.out.println(w == y ); // false
10     }

【2】進入 valueOf 方法:根據原始碼分析:只有當 -128 <= i >= 127 時,就使用享元模式,從快取中獲取值

 1 public static Integer valueOf(int i) {
 2     /**
 3      * IntegerCache.low = -128
 4      * IntegerCache.highhigh = 127
 5      * 根據原始碼分析:只有當 -128 <= i >= 127 時,就使用享元模式,從快取中獲取值
 6      * IntegerCache 相當於工廠類
 7      */
 8     if (i >= IntegerCache.low && i <= IntegerCache.high)
 9         return IntegerCache.cache[i + (-IntegerCache.low)];
10     return new Integer(i);
11 }

【3】我們進入工廠角色:Interger 則為我們的具體享元角色。

 1 private static class IntegerCache {
 2     static final int low = -128;
 3     static final int high;
 4     //工廠類中儲存物件例項的陣列
 5     static final Integer cache[]; 
 6 
 7     static {
 8         ······
 9         high = 127;
10         //定義陣列長度 = 127+128+1
11         cache = new Integer[(high - low) + 1];
12         int j = low;
13         for(int k = 0; k < cache.length; k++)
14             //迴圈建立物件,並放入陣列快取
15             cache[k] = new Integer(j++);
16 
17         // 斷言 如果為true 繼續執行,false 則拋錯
18         assert IntegerCache.high >= 127;
19     }
20 }

七、享元模式的注意事項和細節


1)、對享元模式的理解: “享” 表示共享 “元” 表示物件。
2)、系統中有大量物件,這些物件消耗大量記憶體,並且物件的狀態大部分可以外部化時,我們就可以考慮選用享元模式。
3)、用唯一標識碼判斷,如果記憶體中有則直接返回,一般使用 HashMap、HashTable 或者陣列之內進行儲存。
4)、享元模式提高了系統的複雜度。需要分離內部狀態和外部狀態。而外部狀態具有固化特性,不應該隨著內部狀態的改變而改變,這是使用享元模式需要注意的地方。
5)、使用享元模式時,注意劃分內部狀態和外部狀態,並且需要一個工廠類對享元角色進行管理。