享元(Flyweight)模式:顧名思義就是被共享的單元。意圖是複用物件,節省記憶體,提升系統的訪問效率。比如在紅白機冒險島遊戲中的背景花、草、樹木等物件,實際上是可以多次被不同場景所複用共享,也是為什麼以前的遊戲佔用那麼小的記憶體,卻讓我們感覺地圖很大的原因。
一、享元模式介紹
1.1 享元模式的定義
享元模式的定義是:運用共享技術來有效地支援大量細粒度物件的複用。
這裡就提到了兩個要求:細粒度和共享物件。而正是因為要求細粒度,那麼勢必會造成物件數量過多而且物件性質相近。所以我們可以將物件分為:內部狀態和外部狀態,內部狀態指物件共享出來的資訊,儲存在享元資訊內部,不會隨著環境改變;外部狀態指物件得以依賴的標記,會隨著環境改變,不可以共享。根據是否共享,可以分成兩種模式:
- 單純享元模式:該模式中所有具體享元類都是可以共享,不存在非共享具體享元類
- 複合享元模式:將單純享元物件使用組合模式加以組合,可以形成複合享元物件
實際上享元模式的本質就是快取共享物件,降低記憶體消耗。
1.2 享元模式的結構
我們可以根據享元模式的定義畫出大概的結構圖,如下所示:
FlyweightFactory
:享元工廠,負責建立和管理享元角色Flyweight
:抽象享元,是具體享元類的基類,提供具體享元需要的公共介面SharedFlyweight、UnSharedFlyweight
:具體享元角色和具體非享元類Client
:客戶端,呼叫具體享元和非享元類
1.3 享元模式的實現
根據上面的類圖可以實現如下程式碼:
/**
* @description: 抽象享元類
* @author: wjw
* @date: 2022/4/2
*/
public interface Flyweight {
/**
* 抽象享元方法
* @param state 程式碼外部狀態值
*/
public void operation(int state);
}
/**
* @description: 具體享元類
* @author: wjw
* @date: 2022/4/2
*/
public class SharedFlyweight implements Flyweight{
private String key;
public SharedFlyweight(String key) {
System.out.println("具體的享元類:" + key + "已被建立");
}
@Override
public void operation(int state) {
System.out.println("具體的享元類被呼叫:" + state);
}
}
/**
* @description: 非共享的具體類,並不強制共享
* @author: wjw
* @date: 2022/4/2
*/
public class UnSharedFlyweight implements Flyweight{
public UnSharedFlyweight() {
System.out.println("非享元類已建立");
}
@Override
public void operation(int state) {
System.out.println("我是非享元類" + state);
}
}
/**
* @description: 享元工廠類,負責建立和管理享元類
* @author: wjw
* @date: 2022/4/2
*/
public class FlyweightFactory {
private HashMap<String, Flyweight> flyweights = new HashMap<>();
public FlyweightFactory() {
flyweights.put("flyweight1", new SharedFlyweight("flyweight1"));
}
public Flyweight getFlyweight(String key) {
return flyweights.get(key);
}
}
/**
* @description: 客戶端類
* @author: wjw
* @date: 2022/4/2
*/
public class Client {
public static void main(String[] args) {
FlyweightFactory flyweightFactory = new FlyweightFactory();
Flyweight flyweight1 = flyweightFactory.getFlyweight("flyweight1");
flyweight1.operation(1);
UnSharedFlyweight unSharedFlyweight = new UnSharedFlyweight();
unSharedFlyweight.operation(2);
}
}
測試結果:
具體的享元類:flyweight1已被建立
具體的享元類被呼叫:1
非享元類已建立
我是非享元類2
二、享元模式應用場景
2.1 在文字編輯器中的應用
如果按照每一個字元設定成一個物件,那麼對於幾十萬的文字,儲存幾十萬的物件顯然是不可取,記憶體的利用率也不夠高,這個時候可以將字元設定成一個共享物件,它同時可以在多個場景中使用。不同的場景用字型font、字元大小size和字元顏色colorRGB來進行區分。具體實現如下:
/**
* @description: 字的格式,享元類
* @author: wjw
* @date: 2022/4/2
*/
public class CharacterStyle {
private String font;
private int size;
private int colorRGB;
public CharacterStyle(String font, int size, int colorRGB) {
this.font = font;
this.size = size;
this.colorRGB = colorRGB;
}
@Override
public boolean equals(Object obj) {
CharacterStyle otherCharacterStyle = (CharacterStyle) obj;
return font.equals(otherCharacterStyle.font)
&& size == otherCharacterStyle.size
&& colorRGB == otherCharacterStyle.colorRGB;
}
}
/**
* @description: 字風格工廠類,建立具體的字
* @author: wjw
* @date: 2022/4/2
*/
public class CharacterStyleFactory {
private static final List<CharacterStyle> styles = new ArrayList<>();
public static CharacterStyle getStyle(String font, int size, int colorRGB) {
CharacterStyle characterStyle = new CharacterStyle(font, size, colorRGB);
for (CharacterStyle style : styles) {
if (style.equals(characterStyle)) {
return style;
}
}
styles.add(characterStyle);
return characterStyle;
}
}
/**
* @description: 字類
* @author: wjw
* @date: 2022/4/2
*/
public class Character {
private char c;
private CharacterStyle style;
public Character(char c, CharacterStyle style) {
this.c = c;
this.style = style;
}
@Override
public String toString() {
return style.toString() + c;
}
}
/**
* @description: 編輯輸入字
* @author: wjw
* @date: 2022/4/2
*/
public class Editor {
private List<Character> chars = new ArrayList<>();
public void appendCharacter(char c, String font, int size, int colorRGB) {
Character character = new Character(c, CharacterStyleFactory.getStyle(font, size, colorRGB));
System.out.println(character);
chars.add(character);
}
}
/**
* @description: 客戶端測試類
* @author: wjw
* @date: 2022/4/2
*/
public class EditorTest {
public static void main(String[] args) {
Editor editor = new Editor();
System.out.println("相同的字--------------------------------------");
editor.appendCharacter('t', "宋體", 12, 7777);
editor.appendCharacter('t', "宋體", 12, 7777);
System.out.println("不相同的字------------------------------------");
editor.appendCharacter('t', "宋體", 12, 7777);
editor.appendCharacter('x', "宋體", 12, 7777);
}
}
測試結果如下:
相同的字--------------------------------------
cn.ethan.design.flyweight.CharacterStyle@610455d6t
cn.ethan.design.flyweight.CharacterStyle@610455d6t
不相同的字------------------------------------
cn.ethan.design.flyweight.CharacterStyle@610455d6t
cn.ethan.design.flyweight.CharacterStyle@610455d6x
從結果可以看出,同一種風格的字用的是同一個享元物件。
2.2 在String 常量池中的應用
從上一應用我們發現,很像Java String常量池的應用:對於建立過的String,直接指向呼叫即可,不需要重新建立。比如說這段程式碼:
String str1 = “abc”;
String str2 = “abc”;
String str3 = new String(“abc”);
String str4 = new String(“abc”);
在Java 執行時區域中:
2.3 在Java 包裝類中的應用
在Java中有Short、Long、Byte、Integer等包裝類。這些類中都用到了享元模式,以Integer 為例進行講解。
在介紹前先看看這段程式碼:
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i1 == i2);
System.out.println(i3 == i4);
首先說明“==”是判斷兩個物件儲存的地址是否相同
按照常理,最後輸出應該都是true,然而最後的輸出是:
true
false
這是因為Integer包裝型別的自動裝箱和拆箱、Integer中的享元模式的結果導致的。我們一步步來看:
2.3.1 包裝型別的自動裝箱(Autoboxing)和自動拆箱(Unboxing)
-
自動裝箱
就是自動將基本資料型別裝換成包裝型別。實際上
Integer i1 = 100
底層是Integer i1 = Integer.valueOf(100)
。看看這段原始碼:/** * Returns an {@code Integer} instance representing the specified * {@code int} value. If a new {@code Integer} instance is not * required, this method should generally be used in preference to * the constructor {@link #Integer(int)}, as this method is likely * to yield significantly better space and time performance by * caching frequently requested values. * * This method will always cache values in the range -128 to 127, * inclusive, and may cache other values outside of this range. * * @param i an {@code int} value. * @return an {@code Integer} instance representing {@code i}. * @since 1.5 */ public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
說明在裝箱時,看似相同的值,但是建立了兩個不同的Integer物件,因此兩個100的值自然不相同了。所以上面程式碼建立的物件每個都不相同,所以應該都是false呀,但為什麼
i1
和i2
還是相同的呢?我們再來看中間的這句話:
This method will always cache values in the range -128 to 127,
這個方法總是會快取值在-128到127之間的值,
說明在[-128, 127]範圍內的值,自動裝箱不會建立物件,是利用享元模式進行共享。而
IntegerCache
就相當於生成享元物件的工廠類,我們再看其原始碼:/** * Cache to support the object identity semantics of autoboxing for values between * -128 and 127 (inclusive) as required by JLS. * * The cache is initialized on first usage. The size of the cache * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option. * During VM initialization, java.lang.Integer.IntegerCache.high property * may be set and saved in the private system properties in the * sun.misc.VM class. */ 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() {} }
-
自動拆箱
是自動將包裝型別轉換成基本資料型別。實際上
int j1 = i1
底層是int j1 = i1.intValue()
,我們看其原始碼:/** * Returns the value of this {@code Integer} as an * {@code int}. */ public int intValue() { return value; }
實際上也就是直接返回該值。
回到上面的四行程式碼:
- 前兩行是因為它們的值在[-127, 128]之間,而且由於享元模式,
i1
和i2
共用一個物件,所以結果為true - 後兩行則是因為它們值在範圍之外,所以重新建立不同的物件,因此結果為false
其實在使用包裝類判斷值時,儘量不要使用“==”來判斷,IDEA中也給我們提了醒:
所以說在判斷包裝類時,應該儘量使用"equals"來進行判斷,先判斷兩者是否為同一型別,然後再判斷其值
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
所以對於上面的四行程式碼,最後的結果就都會是true了。
三、享元模式和單例模式、快取的區別
3.1 和單例模式的區別
單例模式中,一個類只能建立一個物件,而享元模式中一個類可以建立多個類。享元模式則有點單例的變體多例。但是從設計上講,享元模式是為了物件複用,節省記憶體,而多例模式是為了限制物件的個數,設計意圖不相同。
3.2 和快取的區別
在享元模式中,我們是通過工廠類來“快取”已經建立好的物件,重點在物件的複用。
在快取中,比如CPU的多級快取,是為了提高資料的交換速率,提高訪問效率,重點不在物件的複用
參考資料
《重學Java設計模式》
《設計模式之美》專欄