HashMap的hash機制詳解
HashMap可謂是面試中的高頻熱點問題了,一般可能也就面試前突擊複習下,背些知識點,面試後可能就忘了,為了做到不遺忘,我們需要徹底弄懂它的機制。
Hash表介紹
首先我們看一張經典的圖:

hash.jpg
這裡有一個大小為16的陣列,比如說我現在有一個整數32,那麼如果使用除餘法,496 % 16 = 0,所以496就應該存在陣列0的位置上。但這個時候如果再存一個896呢? 896%16 = 0,按理應該也放在陣列0位置,但這個位置已經被496佔用了,這種現象被成為hash碰撞,這個時候我麼可以在陣列0後面掛一個連結串列,有碰撞的都往連結串列上加。
以上的除餘法是一種雜湊演算法,碰撞後掛連結串列是一種解決hash碰撞的演算法,叫做拉鍊法。其實還有其他演算法,具體的大家可以自己搜尋 hash表去學習,這裡介紹只是為了給HashMap的講解打個基礎。
HashMap的hash機制
HashMap的儲存形式
通過上面hash表的介紹,我們知道了hash表其實核心儲存還是陣列,HashMap也一樣:
/** * The table, initialized on first use, and resized as * necessary. When allocated, length is always a power of two. * (We also tolerate length zero in some operations to allow * bootstrapping mechanics that are currently not needed.) */ transient Node<K,V>[] table;
當然,這個陣列存的不再是上面的整數那麼簡單,而是一個物件結構:
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; //省略部分程式碼 }
HashMap的雜湊演算法
首先,我們一般是存一個key-value進來,第一步就是用key 的hash值計算一個hashcode出來(怎麼計算後面再說)
那HashMap到底是如何確定將這個hashcode代表的值放在table[]陣列的哪個位置呢?計算方式程式碼很簡單,理解起來確實很難:
int n = table.length; int hash = 通過key計算出來的hash值 int pos = (n-1)&hash
這段神奇的(n-1)&has其實和hash % n 道理是一樣的,但有些地方不一樣,
要說位運算效率高,能實現的黑科技也多,但程式碼確實不好懂,這種計算方式一般人基本都搞不懂,這裡我們重點分析下
以table.length = 16為例,所有的資料最終取餘後都要分佈在16個位置上,從感性上大家都有一種認知:超過了16後,資料太大其實和分佈在0到16之間是一樣的。這種認知反應在二進位制上就是:高位基本沒用,只要低位的資訊就可以了。所以如果想要利用位運算達到去餘的目的,我們就得需要將hash的高位擦除,保留低位。保留多少位數肯定由table.length決定。
那麼如何擦除資料呢?
還是以table.length=16為例,除餘的結果肯定分佈在0到15之間,假設hash值為 10100101 11000100 00100101(二進位制),那麼真正有用的只後面四位(15轉換成二進位制後只有後面4位有效),所以看下這個運算:
10100101 11000100 00100101 &00000000 00000000 00001111 ------------------------------------------------ 00000000 00000000 00001111
可以看到hash&(n-1)實際就是為了保留hash的低位資料,擦除高位資料,很明顯,想要擦除高位,(n-1)的結果低位必須是全1,這也就解釋了為什麼HashMap的table長度始終是2的平方,只有這樣,table.length-1才能保證低位結果是全1。
HashMap的hash優化
從上面我們看到,在確定一個數據的儲存位置時,HashMap只取了低位資料,這樣其實有一個缺陷,那就是我們拋棄了高位資訊,也就拋棄了資料分佈的離散性,如果資料碰巧有了某種規律,每一次都會產生碰撞,這樣就划不來了,一個好的hash雜湊演算法應該要儘量降低碰撞,這個時候我們可以把高位資訊利用起來。
這也是為什麼HashMap不直接使用key的hash值的原因,如果key的hash演算法實現比較糟糕就無法保證資料離散,從而無法減少hash碰撞了。現在我們看下HashMap是怎麼優化的:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
程式碼很簡單,又是一個位操作,現在我們來詳細分析下這樣做的目的,
假設key.hashCode() = 10100101 11000100 00100101
現在h >>> 16 就是無符號右移16位,高位補0
變成了 00000000 00000101 00101100
然後:
10100101 11000100 00100101 ^00000000 00000101 00101100 ------------------------------------------------ 10100101 11000101 00101101
可以看到,這麼做後能夠利用高16位資訊和低16位資訊一起得出一個新的結果,這樣新的低位資訊其實是老的高位資訊和老的低位資訊共同作用的結果,這樣的話就能減少初始key.hashCode()的碰撞可能性。
總結
這裡只是大致的講解了下HashMap的hash機制,核心的擴容,儲存, 查詢,刪除等具體分析還請靜待下一篇文章。