俗話說,自己寫的代碼,6個月後也是別人的代碼……複習!複習!複習!總結的知識點如下: 享元模式概念和實現例子 使用了享元模式的Java API String類 java.lang.Integer 的 valueOf(int)方法源碼分析 使用享元模式的條件 享元模式和單例模式的區別 前面的策略模式的
俗話說,自己寫的代碼,6個月後也是別人的代碼……複習!複習!複習!總結的知識點如下:
- 享元模式概念和實現例子
- 使用了享元模式的Java API String類
- java.lang.Integer 的 valueOf(int)方法源碼分析
- 使用享元模式的條件
- 享元模式和單例模式的區別
前面的策略模式的話題提起了:如何解決策略類膨脹的問題,說到 “有時候可以通過把依賴於環境Context類的狀態保存到客戶端裡面,而將策略類設計成可共用的,這樣策略類實例可以被不同客戶端使用。”換言之,可以使用享元模式來減少對象的數量,享元模式它的英文名字叫Flyweigh模式,又有人翻譯為羽量級模式,它是構造型模式之一,它通過與其他類似對象共用數據來減小記憶體占用,也正應了它的名字:享-分享。
那麼到底是什麼意思呢?有什麼用呢?下麵看個例子:我們有一個文檔,裡面寫了很多英文,大家知道英文字母有26個,大小寫一起一共是52個:
那麼我保存這個文件的時候,所有的單詞都占據了一份記憶體,每個字母都是一個對象,如果文檔里的字母有重覆的,怎麼辦?難道每次都要創建新的字母對象去保存麽?答案是否定的,其實每個字母只需要創建一次,然後把他們保存起來,當再次使用的時候直接在已經創建好的字母里取就ok了,這就是享元模式的一個思想的體現。說到這兒,其實想起了Java的String類,這個類就是應用了享元模式。稍後再說,先看享元模式的類圖和具體實現例子。
抽象享元角色(介面或者抽象類):所有具體享元類的父類,規定一些需要實現的公共介面。
具體享元角色:抽象享元角色的具體實現類,並實現了抽象享元角色規定的方法。
享元工廠角色:負責創建和管理享元角色。它必須保證享元對象可以被系統適當地共用。當一個客戶端對象調用一個享元對象的時候,享元工廠角色會檢查系統中是否已經有一個符合要求的享元對象。如果已經有了,享元工廠角色就應當提供這個已有的享元對象,如果系統中沒有一個適當的享元對象的話,享元工廠角色就應當創建一個合適的享元對象。代碼如下:
1 public interface ICharacter { 2 /** 3 * 享元模式的抽象享元角色,所有具體享元類的父類,規定一些需要實現的公共介面。其實沒有這個介面也可以的。 4 * 5 * 顯式我自己的字母 6 */ 7 void displayCharacter(); 8 } 9 10 public class ChracterBuilder implements ICharacter { 11 private char aChar; 12 13 public ChracterBuilder(char c) { 14 this.aChar = c; 15 } 16 /** 17 * 具體的享元模式角色 18 */ 19 @Override 20 public void displayCharacter() { 21 System.out.println(aChar); 22 } 23 } 24 25 public class FlyWeightFactory { 26 /** 27 * 註意:享元模式採用一個共用來避免大量擁有相同內容對象的開銷。這種開銷最常見、最直觀的就是記憶體的損耗。 28 * 我們這裡使用數組也行,或者 HashMap 29 */ 30 private Map<Character, ICharacter> characterPool; 31 32 public FlyWeightFactory() { 33 this.characterPool = new HashMap<>(); 34 } 35 36 public ICharacter getICharater(Character character) { 37 // 先去pool里判斷 38 ICharacter iCharacter = this.characterPool.get(character); 39 40 if (iCharacter == null) { 41 // 如果池子里沒有就new一個新的,並加到pool里 42 iCharacter = new ChracterBuilder(character); 43 this.characterPool.put(character, iCharacter); 44 } 45 46 // 否則直接從pool里取出 47 return iCharacter; 48 } 49 }View Code
下麵使用客戶端調用,先看看普通的方法
public class MainFlyWeight { public static void main(String[] args) { //=================================== // 不用享元模式,我們每次創建相同內容的字母的時候,都要new一個新的對象 ICharacter iCharacter = new ChracterBuilder('a'); ICharacter iCharacter1 = new ChracterBuilder('b'); ICharacter iCharacter2 = new ChracterBuilder('b'); ICharacter iCharacter3 = new ChracterBuilder('b'); ICharacter iCharacter4 = new ChracterBuilder('b'); iCharacter.displayCharacter(); iCharacter1.displayCharacter(); iCharacter2.displayCharacter(); iCharacter3.displayCharacter(); iCharacter4.displayCharacter(); // 再通過實驗判斷 if (iCharacter2 == iCharacter1) { System.out.print("true"); } else { // 列印了 false,說明是兩個不同的對象 System.out.print("false"); } }View Code
下麵使用享元模式,必須指出的是,使用享元模式,那麼客戶端絕對不可以直接將具體享元類實例化,而必須通過一個工廠得到享元對象。
public class MainFlyWeight { public static void main(String[] args) { // 使用享元模式 // 必須指出的是,客戶端不可以直接將具體享元類實例化,而必須通過一個工廠 FlyWeightFactory flyWeightFactory = new FlyWeightFactory(); ICharacter iCharacter = flyWeightFactory.getICharater('a'); ICharacter iCharacter1 = flyWeightFactory.getICharater('b'); ICharacter iCharacter2 = flyWeightFactory.getICharater('b'); iCharacter.displayCharacter(); iCharacter1.displayCharacter(); iCharacter2.displayCharacter(); if (iCharacter1 == iCharacter2) { // 確實列印了 System.out.print("============"); } // 同樣列印的都一樣,但是對象記憶體的占據卻不一樣了,減少了記憶體的占用 } }View Code
類圖如下:
一般而言,享元工廠對象在整個系統中只有一個,因此也可以使用單例模式,由工廠方法產生所需要的享元對象。且設計模式不用拘泥於具體代碼, 代碼實現可能有n多種方式,n多語言……再看一例子,有老師類,繼承Person類,老師類里保存一個數字編號,客戶端可以通過它來找到對應的老師。
1 public class Person { 2 private String name; 3 4 private int age; 5 6 private String sex; 7 8 /** 9 * person是享元抽象角色 10 * 11 * @param age int 12 * @param name String 13 * @param sex String 14 */ 15 public Person(int age, String name, String sex) { 16 this.age = age; 17 this.name = name; 18 this.sex = sex; 19 } 20 21 public Person() { 22 23 } 24 25 public int getAge() { 26 return age; 27 } 28 29 public void setAge(int age) { 30 this.age = age; 31 } 32 33 public String getName() { 34 return name; 35 } 36 37 public void setName(String name) { 38 this.name = name; 39 } 40 41 public String getSex() { 42 return sex; 43 } 44 45 public void setSex(String sex) { 46 this.sex = sex; 47 } 48 } 49 50 public class Teacher extends Person { 51 private int number; 52 53 /** 54 * teacher是具體的享元角色 55 * 56 * @param number int 57 * @param age int 58 * @param name String 59 * @param sex String 60 */ 61 public Teacher(int number, int age, String name, String sex) { 62 super(age, name, sex); 63 this.number = number; 64 } 65 66 public Teacher() { 67 super(); 68 } 69 70 public int getNumber() { 71 return number; 72 } 73 74 public void setNumber(int number) { 75 this.number = number; 76 } 77 } 78 79 public class TeacherFactory { 80 private Map<Integer, Teacher> integerTeacherMapPool; 81 82 private TeacherFactory() { 83 this.integerTeacherMapPool = new HashMap<>(); 84 } 85 86 public static TeacherFactory getInstance() { 87 return Holder.instance; 88 } 89 90 public Teacher getTeacher(int num) { 91 Teacher teacher = integerTeacherMapPool.get(num); 92 93 if (teacher == null) { 94 // TODO 模擬用,不要把teacher寫死,每次使用set 95 teacher = new Teacher(); 96 teacher.setNumber(num); 97 98 integerTeacherMapPool.put(num, teacher); 99 } 100 101 return teacher; 102 } 103 104 /** 105 * 使用靜態內部類,靜態內部類相當於外部類的static域,它的對象與外部類對象間不存在依賴關係,因此可直接創建。 106 * 因為靜態內部類相當於其外部類的成員,所以在第一次被使用的時候才被會裝載。且只裝載一次。 107 * 而對象內部類的實例,是綁定在外部對象實例中的。 108 * 靜態內部類中可以定義靜態方法,在靜態方法中只能夠引用外部類中的靜態成員方法或者成員變數。 109 * 110 * 在某些情況中,JVM 含地了同步,這些情況下就不用自己再來進行同步控制了。這些情況包括: 111 *1.由靜態初始化器(在靜態欄位上或static{}塊中的初始化器)初始化數據時 112 *2.訪問final欄位時 113 *3.在創建線程之前創建對象時 114 *4.線程可以看見它將要處理的對象時 115 * 116 * 故,我使用了靜態初始化器來實現線程安全的單例類,它由 JVM 來保證線程安全性。 117 * 且這種實現方式,會在類裝載的時候(使用這個類的時候)就初始化對象,不管使用者需要不需要,且只實例化一次。 118 * 119 * 故,我在外部類里再創建一個靜態內部類,在靜態內部類里去創建本類(外部類)的對象,這樣只要不使用這個靜態內部類,那就不創建對象實例,從而同時實現延遲載入和線程安全。 120 */ 121 private static class Holder { 122 private static final TeacherFactory instance = new TeacherFactory(); 123 } 124 }View Code
客戶端調用
public class MainClass { public static void main(String[] args) { // 先創建工廠 TeacherFactory teacherFactory = TeacherFactory.getInstance(); // 通過工廠得到具體對象 Teacher teacher = teacherFactory.getTeacher(1000); Teacher teacher1 = teacherFactory.getTeacher(1001); Teacher teacher2 = teacherFactory.getTeacher(1000); System.out.println(teacher.getNumber()); System.out.println(teacher1.getNumber()); System.out.println(teacher2.getNumber()); // 判斷是否是相等對象 if (teacher == teacher2) { // 確實列印了,ok System.out.print("____________-"); } } }View Code
類圖
小結,到底系統需要滿足什麼樣的條件才能使用享元模式。對於這個問題,總結出以下幾點:
- 一個系統中存在著大量的細粒度對象,且這些細粒度對象耗費了大量的記憶體。
- 這些細粒度對象的狀態中的大部分都可以外部化
- 這些細粒度對象可以按照內蘊狀態分成很多的組,當把外蘊對象從對象中剔除時,每一個組都可以僅用一個對象代替。
- 軟體系統不依賴於這些對象的身份,換言之,這些對象可以是不可分辨的。
滿足以上的這些條件的系統可以使用享元模型。最後,使用享元模式需要維護一個記錄了系統已有的所有享元的哈希表,也稱之為對象池,而這也需要耗費一定的資源。因此應當在有足夠多的享元實例可供共用時才值得使用享元模式。
好了,現在多了幾個新的概念(外部化,內蘊,外蘊……),再次用教科書的理論,分析之前的享元模式的例子和概念:
內蘊狀態:存儲在享元對象內部的對象,並且這些對象是不會隨環境的改變而有所不同。因此,一個享元可以具有內蘊狀態並可以共用。
外蘊狀態:隨環境的改變而改變、不可以共用的對象。享元對象的外蘊狀態必須由客戶端保存,併在享元對象被創建之後,在需要使用的時候再傳入到享元對象內部。外蘊狀態不可以影響享元對象的內蘊狀態,它們是相互獨立的。
享元對象能做到共用的關鍵是區分內蘊狀態(Internal State)和外蘊狀態(External State)。回憶之前的例子:最近的Teacher例子,具體享元角色類Teacher類其實就是有一個內蘊狀態,在本例中一個int類型的number屬性,它的值應當在享元對象被創建時賦予,也就是內蘊狀態對象讓享元對象自己去保存,且可以被客戶端共用,所有的內蘊狀態在對象創建之後,就不會再改變了。下麵看一個具有外蘊狀態的享元模式:
public interface BaseDao { /** * 連接數據源,享元模式的抽象享元角色 * * @param session String 和數據源連接的session,該參數就是外蘊狀態 */ void connect(String session); }View Code
如果一個享元對象有外蘊狀態的話,所有的外部狀態都必須存儲在客戶端,在使用享元對象時,再由客戶端傳入享元對象。這裡只有一個外蘊狀態,connect()方法的參數就是由外部傳入的外蘊狀態
public class DaoA implements BaseDao { /** * 內蘊狀態 */ private String strConn = null; /** * 內蘊狀態在創建享元對象的時候作為參數傳入構造器 * * @param s String */ public DaoA(String s) { this.strConn = s; } /** * 外蘊狀態 session 作為參數傳入抽象方法,可以改變方法的行為,但是對於內蘊狀態不做改變,兩者獨立 * 外蘊狀態(對象)存儲在客戶端,當客戶端使用享元對象的時候才被傳入享元對象,而不是開始就有。 * * @param session Session 和數據源連接的session,該參數就是外蘊狀態 */ @Override public void connect(String session) { System.out.print("內蘊狀態 是" + this.strConn); System.out.print("外蘊狀態 是" + session); } }View Code
享元工廠
public enum Factory { /** * 單例模式的最佳實現是使用枚舉類型。只需要編寫一個包含單個元素的枚舉類型即可 * 簡潔,且無償提供序列化,並由JVM從根本上提供保障,絕對防止多次實例化,且能夠抵禦反射和序列化的攻擊。 */ instance; /** * 可以有自己的操作 */ private Map<String, BaseDao> stringBaseDaoMapPool = new HashMap<>(); public BaseDao factory(String s) { BaseDao baseDao = this.stringBaseDaoMapPool.get(s); if (baseDao == null) { baseDao = new DaoA(s); this.stringBaseDaoMapPool.put(s, baseDao); } return baseDao; } }View Code
雖然客戶端申請了三個享元對象,但是實際創建的享元對象只有兩個,這就是共用的含義
public class Client { public static void main(String[] args) { BaseDao baseDao = Factory.instance.factory("A連接數據源"); BaseDao baseDao1 = Factory.instance.factory("B連接數據源"); BaseDao baseDao2 = Factory.instance.factory("A連接數據源"); baseDao.connect("session1"); baseDao1.connect("session2"); baseDao2.connect("session1"); if (baseDao == baseDao2) { // 列印了 System.out.print("==========="); } } }View Code
在JDK中有哪些使用享元模式的例子?舉例說明。
說兩個,第一個是String類,第二個是java.lang.Integer 的 valueOf(int)方法。針對String,也是老生常談了,它是final的,字元串常量通常是在編譯的時候就確定好的,定義在類的方法區里,也就是說,不同的類,即使用了同樣的字元串, 還是屬於不同的對象。所以才需要通過引用字元串常量來減少相同的字元串的數量。
String s1 = "hello"; String s2 = "he" + "llo"; if (s1 == s2) { System.out.print("====");// 列印了,說明是1,2引用了一個對象hello }View Code
使用相同的字元序列而不是使用new關鍵字創建的兩個字元串會創建指向Java字元串常量池中的同一個字元串的指針。字元串常量池是Java節約資源的一種方式。其實就是使用了享元模式的思想。字元串的分配,和其他的對象分配一樣,耗費高昂的時間與空間代價。JVM為了提高性能和減少記憶體開銷,在實例化字元串常量的時候進行了一些優化。為了減少在JVM中創建的字元串的數量,字元串類維護了一個字元串池,每當代碼創建字元串常量時,JVM會首先檢查字元串常量池。如果字元串已經存在池中,就返回池中的實例引用。如果字元串不在池中,就會實例化一個字元串並放到池中。Java能夠進行這樣的優化是因為字元串是不可變的,可以不用擔心數據衝突進行共用。
java.lang.Integer 的 valueOf(int)方法源碼分析(8.0版本)
Integer a = 1; Integer b = 1; System.out.print(a == b);// trueView Code
再看一例子;
Integer a = new Integer(1); Integer b = new Integer(1); System.out.print(a == b);// falseView Code
很容易理解,再看
Integer a = 200; Integer b = 200; System.out.println(a == b);// falseView Code
怎麼還是false呢?看看到底發生了什麼,反編譯上述程式;
public static main([Ljava/lang/String;)V L0 LINENUMBER 19 L0 SIPUSH 200 INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer; ASTORE 1 L1 LINENUMBER 20 L1 SIPUSH 200 INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer; ASTORE 2View Code
我發現每次都是使用了自動裝箱
Integer c = Integer.valueOf(200);
再看該方法源碼;
public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }View Code
我發現,當使用Integer的自動裝箱的時候,值在low和high之間時,會用緩存保存起來,供多次使用,以節約記憶體。如果不在這個範圍內,則創建一個新的Integer對象。這不就是尼瑪享元模式嗎!看看範圍:-128~+127
private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } private IntegerCache() {} }View Code
享元模式的缺陷是什麼?
享元模式的優點在於它大幅度地降低記憶體中對象的數量。但是,它做到這一點所付出的代價也是很高的:
● 享元模式使得系統更加複雜。為了使對象可以共用,需要將一些狀態外部化,這使得程式的邏輯複雜化。
● 享元模式將享元對象的狀態外部化,而讀取外部狀態使得運行時間稍微變長。
享元模式比起工廠,單例,策略,裝飾,觀察者等模式,其實不算是常用的設計模式,它主要用在底層的設計上比較多,比如之前提到的String類,Integer的valueOf(int)方法等。好了,享元模式到這裡總結的差不多了,記得之前有個老師的例子,對老師的工廠類使用了單例模式創建了工廠對象,後來又有一個BaseDao例子,工廠Factory類使用了枚舉作為單例模式的實現,那麼這裡還要順便總結一個老生常談,但是又不見得真的談對了的設計模式——單例模式,如下之前總結的C++版的; 軟體開發常用設計模式—單例模式總結,發現Java實現的單例模式和C++的線上程安全上還是有些區別的。下麵主要說下Java版的單例模式的線程安全性。之前的一些私有靜態屬性(餓漢式),雙重檢測加鎖……不再贅述。
簡單看看,單例模式還得單開文章總結,涉及到了枚舉實現和記憶體模型:單例類只能有一個實例,單例類必須自己創建自己的唯一實例,單例類必須給所有其他對象提供這一實例(提供全局訪問點)。
小結:享元模式和單例模式的異同
享元是對象級別的, 也就是說在多個使用到這個對象的地方都只需要使用這一個對象即可滿足要求, 而單例是類級別的, 就是說這個類必須只能實例化出來一個對象, 可以這麼說, 單例是享元的一種特例, 設計模式不用拘泥於具體代碼, 代碼實現可能有n多種方式, 而單例可以看做是享元的實現方式中的一種, 但是他比享元更加嚴格的控制了對象的唯一性。