設計模式——享元模式
享元模式(Flyweight Pattern):主要用於減少建立物件的數量,以減少記憶體佔用和提高效能。在面向物件程式的設計過程中,有時需要建立大量相似的物件例項。如果都建立將會消耗很多系統資源,它是系統性能提高的一個瓶頸。但如果將這些物件的相似部分抽取出來共享,則能節約大量的系統資源,這就是享元模式的產生背景。在 Java 中 String 值的儲存就使用了享元模式,相同的值只存一個。
一、基本介紹
1、享元模式(Flyweight Pattern)也叫 “蠅量模式”:運用共享技術有效地支援大量細粒度物件。
2、常用於系統底層開發,解決系統的效能問題。像資料庫連線池,裡面都是建立好的連線物件,在這些連線物件中有我們需要的則直接拿過來用,避免重新建立,如果沒有我們需要的,則建立一個。
3、享元模式能夠解決重複物件的記憶體消耗問題,當系統中有大量相似物件,需要緩衝池時。不需要總建立新物件,可以從緩衝池裡拿。這樣可以降低系統記憶體,同時提高效率。
4、享元模式經典的應用的場景就是池技術,String 常量池、資料庫連線池、緩衝池等等都是享元模式的應用,享元模式是池技術的重要實現方式。
二、享元模式的特點
享元模式的主要優點:相同物件只要儲存一份,降低了系統中物件的數量,從而降低了系統中細粒度物件給記憶體帶來的壓力。
享元模式的主要缺點:為了使物件共享,需要將一些不能共享的狀態外部化,這將增加程式的複雜性。需要分離出外部狀態和內部狀態,而且外部狀態具有固有化性質,不應該隨著內部狀態的變化而變化,否則會造成系統的混亂。
享元模式的主要意圖:執行享元模式有效地支援大量細粒度物件。
享元模式主要解決的問題:在有大量相似物件時,有可能會造成記憶體溢位,我們把其中共同的部分抽取出來,如果有相同的業務請求,直接返回記憶體中已有的物件,避免重新建立。
享元模式如何解決問題:用唯一標識碼判斷,如果記憶體中有,則返回唯一標識所標識的物件。
享元模式關鍵程式碼
享元模式使用場景: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)、使用享元模式時,注意劃分內部狀態和外部狀態,並且需要一個工廠類對享元角色進行管理。