HashMap之深入理解
容量(capacity):雜湊表中容器的數量,初始容量只是雜湊表在建立時的容量。
負載因子(load factor):雜湊表在其容量自動增加之前可以達到多滿的一種尺度。當雜湊表中的條目數超出了負載因子與當前容量的乘積時,則要對該雜湊表進行 rehash 操作(即重建內部資料結構),從而雜湊表將具有大約兩倍的容器數量。
可以說容量與負載因子的合理設定與否直接影響著HashMap的效能指標,我們在初始化時可以指定這兩個引數:
Java程式碼- //capacity=32,load factor=0.75
-
Map map=new HashMap<String,String>(32
此時HashMap會呼叫過載構造方法進行初始化,過載建構函式原始碼如下:
Java程式碼- /**
- * 使用指定容量和負載因子構建一個空的HashMap例項
- */
- public HashMap(int initialCapacity, float loadFactor) {
- if (initialCapacity < 0)
- throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
-
if
- initialCapacity = MAXIMUM_CAPACITY;
- if (loadFactor <= 0 || Float.isNaN(loadFactor))
- throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
- // Find a power of 2 >= initialCapacity
- int capacity = 1;
-
while
- capacity <<= 1;
- this.loadFactor = loadFactor;
- threshold = (int) (capacity * loadFactor);
- table = new Entry[capacity];
- init();
- }
過載構造方法會根據我們指定的容量與負載因子進行了初始化操作。
我們通過以下幾個實驗來探究下容量與負載因子對HashMap例項效能的影響:
1)實驗一
實驗原理:建立幾個HashMap例項,初始化時指定不同的容量大小,並且它們採用預設相同的負載因子,向每個HashMap例項中新增1000000個元素,計算總用時,每個實驗進行一定次數,得出對比分析圖。
實驗結果:
圖中藍色部分代表指定較小容量值;圖中紅色部分代表指定中等容量值;圖中綠色部分代表指定合理容量值;
實驗侷限性:實驗過程中會與使用機器硬體及相關引數設定是否合理有關,而且實驗過程中可能會出現垃圾收集導致返回時間較長的情況,所以需要更大範圍與更多次數的對比分析。但實驗的結果方向性是正確的,可以作為參考。
實驗結論:從實驗結果可以很直觀的看出,當我們指定的HashMap例項大小越接近實際使用大小,HashMap的效能越好。所以在載入因子不變的情況下,指定更合理的大小值可以有效的提高HashMap例項的效能。
2)實驗二
實驗原理:建立幾個HashMap例項,初始化時指定相同的容量大小,不同的負載因子,向每個HashMap例項中新增1000000個元素,計算總用時,每個實驗進行一定次數,得出對比分析圖。
實驗過程:
(1)預設長度為16,建立4個HashMap例項,它們的負載因子分別為0.25,0.5,0.75和1,程式碼如下:
Java程式碼- Map map1 = new HashMap<String, String>(16,0.25f);
- Map map2 = new HashMap<String, String>(16,0.5f);
- Map map3 = new HashMap<String, String>(16,0.75f);
- Map map4 = new HashMap<String, String>(16,1f);
(2)不斷向幾個例項中填充元素,得到實驗結果:
圖中幾條曲線是指定初始化容量較小時,不同負載因子對例項效能的影響,從中可以看出負載因子為0.5時這個HashMap的例項效能最好。負載因子為1時效能最差。
(3)指定HashMap例項初始容量為1232896,建立4個HashMap例項,它們的負載因子分別為0.25,0.5,0.75和1,程式碼如下:
Java程式碼- Map map1 = new HashMap<String, String>(1232896,0.25f);
- Map map2 = new HashMap<String, String>(1232896,0.5f);
- Map map3 = new HashMap<String, String>(1232896,0.75f);
- Map map4 = new HashMap<String, String>(1232896,1f);
(4)不斷向幾個例項中填充元素,得到實驗結果:
圖中幾條曲線是指定初始化容量較大時,不同負載因子對例項效能的影響,從中可以看出負載因子為0.25時這個HashMap的例項效能最差,其他負載因子的效能差不多。
實驗結果:當新增元素較少時可以使用預設負載因子,此時效能差異並不明顯;當新增元素較多時,可以通過測試或估算取得較佳的負載因子值。
實驗侷限性:實驗過程中受新增元素數量的影響較大,元素數量級越大載入因子影響越大。
實驗結論: 測試受多方面因素影響,新增不同數量元素的情況下相同的負載因子在效能影響上並不一致,只有根據實際運用情況來合理配置容量與負載因子的乘積才會使HashMap例項的效能更優,無法判斷的情況下以預設值為最佳。
綜合結論:為了提高HashMap例項的效能,我們可以在初始化時估算出要新增元素的數量,從而指定足以容納這些元素的HashMap容量數值,負載因子在元素個數不同的情況下對HashMap例項的影響較大,再未通過測試的情況下可以採用預設值(未必是最優值),否則將降低例項的效能。
HashMap中的相關計算方法
在HashMap實現中有一些特定的方法,諸如計算下標,計算hash值等,這些方法被用於特定的場合,雖算不上比較複雜的演算法,但重要性不可小看。
1.indexFor方法
indexFor(int h, int length)方法根據指定hash值和陣列長度計算出該hash值所對應陣列下標。indexFor在獲取、新增元素等方法中擔任重要角色。
以下是indexFor方法的原始碼:
Java程式碼- /**
- * 根據hash值h與陣列長度length計算下標位置
- */
- static int indexFor(int h, int length) {
- return h & (length - 1);
- }
核心程式碼只有一行,但是卻不要小看它。indexFor採用的是“按位與”的方式來計算出下標位置,“&”的操作其實就是將十進位制的數轉換成二進位制後兩個二進位制數按相同位進行“與”計算,相同值“與”計算後結果不變,不同值“與”計算後結果為0.
注意:因為下標是從0開始的,以長度16為例,下標範圍為0-15,所以程式碼中使用的是length-1
以length=16為例,計算hash值為12,15,46,90的index值
幾個hash值對應二進位制表示為:
十進位制hash值 | 二進位制值 |
12 | 1100 |
15 | 1111 |
90 | 1011010 |
234 | 11101010 |
此時h & (length - 1)結果即為(以hash為12為例):
12 & 15:
結果:12
類似的其他結果如下:
十進位制hash值 | 二進位制值 | 計算結果 |
12 | 1100 | 12 |
15 | 1111 | 15 |
90 | 1011010 | 10 |
234 | 11101010 | 10 |
總結:indexFor方法採用“&”操作最大的優點就是在計算出下標位置的同時又注重了高效率。
2.hash方法
HashMap為什麼要單獨提供一個hash方法?這個hash方法又有什麼不同?
以下是hash方法的原始碼:
Java程式碼- static int hash(int h) {
- h ^= (h >>> 20) ^ (h >>> 12);
- return h ^ (h >>> 7) ^ (h >>> 4);
- }
hash方法是對已給出hashCode值的補充,用於防止那些質量較差的hash函式。並且將此hash函式作為已給定的hashCode的一個補充,可以提高hash值的質量。hash質量的好壞是非常重要,
HashMap用2的次冪作為表的hash長度,這就容易產生衝突。注意:Null 的key的hash值總是0,即他在
table的索引值永遠為0。
hash方法如何提高hash值的質量,通過以下例項就會很好的瞭解。
1)初始化一個HashMap例項,指定容量為16:
Java程式碼- Map map=HashMap(16);
2)向此例項中新增幾個元素,它們key的hashcode分別為12,28,44和92。
3)通過indexFor方法可以計算出該key所應儲存的下標位置,如果只是通過key的hashcode來計算的話,得出結果為:
key的hashCode | 二進位制值 | length-1二進位制值 | 計算後下標位置 |
12 | 1100 | 1111 | 12 |
28 | 11100 | 1111 | 12 |
44 | 101100 | 1111 | 12 |
92 | 1011100 | 1111 | 12 |
4)從結果可以看出雖然每個key擁有不同的hashCode,但是通過計算後它們卻被分配到了同一下標位置,這顯而易見不是我們想要的結果。
原因就在於indexFor方法的設計思路是“按位與”,這樣計算過程中無論是0&1還是1&0結果都是0.所以幾個key的二進位制值與15的二進位制值(1111)進行“與”操作時,實際就相當於只計算了最後四位有效位(紅色部分),最終出現了上述結果。
hash函式正是為了避免這一點:
key的hashCode | 二進位制值 | hash計算後 | length-1二進位制值 | 計算後下標位置 |
12 | 1100 | 1100 | 1111 | 12 |
28 | 11100 | 11101 | 1111 | 13 |
44 | 101100 | 101110 | 1111 | 14 |
92 | 1011100 | 1011001 | 1111 | 9 |