設計模式 --面試高頻之享元模式
阿新 • • 發佈:2020-03-14
# 前言
享元模式是非常常用的一種結構性設計模式。
特別是在面試的時候。當我們把這一節內容掌握,我相信不管是工作中還是面試中這一塊內容絕對是一大亮點。
# 什麼是享元模式
所謂“享元”,顧名思義就是被共享的單元。享元模式的意圖是複用物件,節省記憶體,前提是享元物件是不可變物件。
具體來講,當一個系統中存在大量重複物件的時候,如果這些重複的物件是不可變物件,我們就可以利用享元模式將物件設計成享元,在記憶體中只保留一份例項,供多處程式碼引用。這樣可以減少記憶體中物件的數量,起到節省記憶體的目的。
這裡值得注意的是只保留一份例項,供多人使用。
# 面試最常見的面試題
我相信大夥在面試的時候經常會被問到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=} 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 類中要共享的物件,是在類載入的時候,就集中一次性建立好的。
但是,對於字串來說,我們沒法事先知道要共享哪些字串常量,所以沒辦法事先建立好。
只能在某個字串常量第一次被用到的時候,儲存到常量池中,當之後再用到的時候,直接引用常量池中已經存在的即可,就不需要再重新建立了
# 實際運用
我們想想,什麼情況我們應該使用享元模式。
我總結了一下:
1. 首先這個物件在很多地方都得使用,否則就是過度設計。
2. 其次這個物件是不可變的,可以讓多個執行緒同時使用。
我舉一個具體的例子。
比如我們開發一個麻將遊戲。沒一局遊戲是不是要new一個麻將桌,new一副麻將。假如同時線上100w人,那我們就new了25w個麻將桌和25w副麻。
我們仔細想想能不能用享元模式來優化,首先麻將桌應該是不能優化的,因為他得記錄我們每一局遊戲得狀態,桌上麻將的情況,等等資訊。但是麻將我們卻可以快取一副,讓他不可變。所有人共用這一副快取的麻將。
# 總結
享元模式其實開發中我們用的不是特別多,但是當需要時,卻非常的有效。包括面試中關於String,基本型別的包裝類關於享元模式的運用。當面試管再丟擲這個問題,如果你能回答清楚並且提出其設計模式是享元模式,我相信一定會讓面試官眼前