前言 享元模式是非常常用的一種結構性設計模式。 特別是在面試的時候。當我們把這一節內容掌握,我相信不管是工作中還是面試中這一塊內容絕對是一大亮點。 什麼是享元模式 所謂“享元”,顧名思義就是被共用的單元。享元模式的意圖是復用對象,節省記憶體,前提是享元對象是不可變對象。 具體來講,當一個系統中存在大量 ...
前言
享元模式是非常常用的一種結構性設計模式。
特別是在面試的時候。當我們把這一節內容掌握,我相信不管是工作中還是面試中這一塊內容絕對是一大亮點。
什麼是享元模式
所謂“享元”,顧名思義就是被共用的單元。享元模式的意圖是復用對象,節省記憶體,前提是享元對象是不可變對象。
具體來講,當一個系統中存在大量重覆對象的時候,如果這些重覆的對象是不可變對象,我們就可以利用享元模式將對象設計成享元,在記憶體中只保留一份實例,供多處代碼引用。這樣可以減少記憶體中對象的數量,起到節省記憶體的目的。
這裡值得註意的是只保留一份實例,供多人使用。
面試最常見的面試題
我相信大伙在面試的時候經常會被問到String,Integer相關的面試題。
那我們就從這兩塊內容開始講解。
享元模式在Integer中的應用
我們先來看下麵這樣一段代碼。
Integer i1 = 56;
Integer i2 = 56;
Integer i3 = 129;
Integer i4 = 129;
System.out.println(i1 == i2); //true
System.out.println(i3 == i4); //false
我相信很多人在面試的時候會遇到這種題目。答案可能會出乎我們的意料。第一個為true,第二個為false。
這正是因為 Integer,用到了享元模式來複用對象,才導致了這樣的運行結果。當我們通過自動裝箱,也就是調用 valueOf() 來創建 Integer 對象的時候,如果要創建的 Integer 對象的值在 -128 到 127 之間,會從 IntegerCache 類中直接返回,否則才調用 new 方法創建。看代碼更加清晰一些,Integer 類的 valueOf() 函數的具體代碼如下所示:
//從這裡的源碼我們能看到,當我們執行Integer i2 = 56;
//這行代碼的時候。其實是通過自動裝箱機制,調用的valueOf。
//當數據在IntegerCache.low~IntegerCache.high之間的時候,我們是直接從緩存中拿取的數據。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
那這個IntegerCache是什麼呢?這個其實是Integer的內部類。
我們挑選重點代碼來看看,源碼如下:
/**
* 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() {}
}
這個是Integer的靜態內部類,當我們載入Ineger的時候該類也會被載入進去。可以看到他緩存了-128 到 127 之間的整型值。
實際上,除了 Integer 類型之外,其他包裝器類型,比如 Long、Short、Byte 等,也都利用了享元模式來緩存 -128 到 127 之間的數據。比如,Long 類型對應的 LongCache 享元工廠類及 valueOf() 。
其實jdk考慮的很周到,我們大部分時間創建出來的Ineger對象,其實都是存儲整型都不是特別大。所以乾脆取一段大小合理的數據直接緩存下來。
舉一個極端一點的例子,假設程式需要創建 1 萬個 -128 到 127 之間的 Integer 對象。使用第一種創建方式,我們需要分配 1 萬個 Integer 對象的記憶體空間;使用後兩種創建方式,我們最多只需要分配 256 個 Integer 對象的記憶體空間。
享元模式在String中的應用
我們都知道String是被final修飾的,大家又仔細想過這其中的緣由嗎?
這最大的原因就是為了實現字元串池化技術。其核心思想就是享元模式。
我們前面提到過享元對象都是不可變的。這樣我們才能保證大家在共同使用的時候不會出現問題。所以String是被final修飾的。
我們再來看一下這段代碼:
String s1 = "享元模式";
String s2 = "享元模式";
String s3 = new String("享元模式");
System.out.println(s1 == s2); //ture
System.out.println(s1 == s3); //false
前兩個s1和s2都是指向的字元串常量池的"享元模式"。而s3指向的是堆的String。
String 類的享元模式的設計,跟 Integer 類稍微有些不同。
Integer 類中要共用的對象,是在類載入的時候,就集中一次性創建好的。
但是,對於字元串來說,我們沒法事先知道要共用哪些字元串常量,所以沒辦法事先創建好。
只能在某個字元串常量第一次被用到的時候,存儲到常量池中,當之後再用到的時候,直接引用常量池中已經存在的即可,就不需要再重新創建了
實際運用
我們想想,什麼情況我們應該使用享元模式。
我總結了一下:
- 首先這個對象在很多地方都得使用,否則就是過度設計。
- 其次這個對象是不可變的,可以讓多個線程同時使用。
我舉一個具體的例子。
比如我們開發一個麻將游戲。沒一局游戲是不是要new一個麻將桌,new一副麻將。假如同時線上100w人,那我們就new了25w個麻將桌和25w副麻。
我們仔細想想能不能用享元模式來優化,首先麻將桌應該是不能優化的,因為他得記錄我們每一局游戲得狀態,桌上麻將的情況,等等信息。但是麻將我們卻可以緩存一副,讓他不可變。所有人共用這一副緩存的麻將。
總結
享元模式其實開發中我們用的不是特別多,但是當需要時,卻非常的有效。包括面試中關於String,基本類型的包裝類關於享元模式的運用。當面試管再拋出這個問題,如果你能回答清楚並且提出其設計模式是享元模式,我相信一定會讓面試官眼前一亮。