這次不以面試背題為目的,挑幾個源碼實現中值得玩味的點來分析一下。
首先看幾個初始化參數,在實現中 Lea 大爺大量的使用了二進制位移運算。比如 16 表示為 1<<4 ,1 073 741 824 表示為 1<<30 。由於計算機的物理特性,二進制運算的效率尤其是位移運算是高於直接使用十進制運算的。在非官方統計中,位運算比取余運算可以節約大約四十余個 CPU 晶振周期,按照Bruce Eckel給出的數據,性能大約可以提升5~8倍。在日常 CURD 的過程中,也可以在寫好注釋的前提下盡量的使用二進制位運算替代十進制運算。
負載因子(LOAD_FACTOR)
初始容量為 16,負載因子為 0.75。這是從 HashMap 誕生開始就沒變過的定義,當元素數量達到當前容量的 75% 時,HashMap 會對數組進行擴容。該因子可在創建實例時指定。
由於哈希值在計算時需要映射到長度為 capacity 的數組下標上,因此哈希值的計算必然包含對數組長度的取模(取模的點后面說),在 put 方法中可以找到計算數組下標時需要對數組長度取模:
因此數組的剩余空間越小,數組下標沖突的幾率越大,元素不得不存儲在鏈表中,勢必會降低插入、刪除和查詢效率。該值越高,數組的利用率也越高,但產生哈希沖突的概率也會隨之變大。
在實際使用中,如果空間較緊張、且對時間要求不高時,可適當的調大該參數。
反之在空間較寬裕、且對時間要求較高時,可適當調小該參數。
取模運算
從上面的取模代碼可以看出,HashMap 中 哈希值與容量的取模不是直接用的取模運算,而是用的與運算:(n-1)& hash 。
將取模運算轉化為與運算是需要條件的,在 n 為 2 的整數冪時, hash%n = (n-1) & hash 的等式才成立。
哈希運算
哈希運算使用了對象本身的 hashcode 方法,用結果本身的高位與低位進行位運算。
在上一節的取模運算中,取模運算被降級為了與運算。這樣在取模時,高位便不會參與運算。高位特征的丟失會增大取模時沖突的概率。
對此大爺采用了“防御性編程”策略,將哈希值的高位與低位進行異或來做二次哈希,這樣,低位的值既包含低位的信息,也包含高位的信息,降低了下標沖突的概率。
素數在取模中的應用
在常識下,對一個數取模時,使用素數作為底數可以降低哈希沖突的概率。
證明取模時使用素數可以降低哈希沖突的概率,需要使用到數論中的“同余數”理論。
通過取余的方式,可以將一個大集合 A 映射到一個小集合 B 。
在 N 與 M 的最大公約數為 1 時,N%M 全體結果的集合為 R { 0,1,2,3...M-1 } 。R 中每一個元素 r 代表一個同余類 N 的集合。
比如對於 M = 3 , r 為 0 代表着集合 N { 3,6,9,12 ... 3*n } 。
假設 N = kn , M = km 。N 與 M 的最大公約數為 k 。則:
N%M = r >> N = Mq+r >> kn = kmq + r
其中 q 是商, r 是余數 。r 的取值范圍為 { 0,1,2,3 ... M-1 }
但是因為 k 的存在: n = mq + r/k >> r = k(n-mq),也就是說,r 必為 k 的整數倍。
r 的取值范圍縮減到了 { 0,1*k,2*k,3*k... } ,取值范圍縮小了 k 倍。那么隨着最大公因數的增大,沖突概率會成倍的增加。
所以在 N 與 M 最大公約數為 1 時,M 是素數還是合數壓根不影響沖突的概率。
但在 N 與 M 最大公約數不是 1 時,會成倍的提高哈希沖突的概率。因此 M 選擇素數,保證與 N 的最大公約數是 1 ,是可以降低哈希沖突的概率的。
HashTable 中以素數作為容量就是該原理的應用。
數組長度
之前介紹過,HashMap 的初始容量為 16。而在進行擴容時:
在 resize 方法中也可以看到,每次擴容后的容量為原容量的兩倍。 16 是 2 的 4 次冪,在該策略下,數組的容量會一直是 2 的整數冪。
這與前面說的,盡量以素數為底數取模的原則相悖。
但是前面也提到過,哈希運算中的取模運算被降級為了與運算,提高了 5 ~ 8 倍的效率,而這種轉化只有在底數為二的整數冪時才成立。
所以可以想到,容量取 2 的整數冪是出於提高取模效率的考量。
而對於哈希沖突,任何哈希算法都不可能完全的避免哈希沖突,因此在設計哈希表時必須設計沖突的處理方式。HashMap 中使用了拉鏈法來處理哈希沖突,同 hash 值的元素用鏈表連起來,在查找時一一比對。因此為了其它單元的效率,在一定程度上增加可控的沖突概率並不是不可行的。但是 Lea 大爺是如何得出該設計的綜合效率會高於維護素數容量的綜合效率,就不得而知了。
TREEIFY_THRESHOLD
jdk 1.7 及之前,處理哈希沖突的方式是拉鏈法。
但 1.8 中對拉鏈法中的鏈表做了進一步優化,默認的當鏈表長度大於 8 時,鏈表會轉化為一顆紅黑樹,提高查詢的效率。
可以想象在元素比較少時,構建紅黑樹的開銷可能會大於紅黑樹帶來的查詢收益。
另外,紅黑樹不是完全平衡的,左右子樹的高度差最大為兩倍。因此在鏈表長度小於 8 時,樹的高度小於 3 層,元素集中在一側子樹中的話在效率提升上並不明顯。
因此在元素較少時采用鏈表存儲,在元素較多時采用紅黑樹存儲。至於長度的閾值為什么是 8 ,只能靠上面的猜測了。