在介紹HashMap的同時,我會把它和HashTable以及ConcurrentHashMap的區別也說一下,不過本文主要是介紹HashMap,其實它們的原理差不多,都是數組加鏈表的形式儲存資料,另外本文所介紹的都是JDK1.8版本的。在介紹之前,先看下Map家族的繼承體系圖:
其中,TreeMap是基於樹實現的,其他三個都是雜湊表結構。
HashMap和Hashtable的主要區別是:
1. Hashtable是安全執行緒,而HashMap則非安全執行緒,Hashtable的實現方法裡面大部分都添加了synchronized關鍵字來確保線程同步,因此相對而言HashMap效能會高一些,在多線程環境下若使用HashMap需要使用Collections.synchronizedMap()方法來擷取一個安全執行緒的集合。
2. HashMap可以使用null作為key,而Hashtable則不允許null作為key。
3. HashMap的初始容量為16,Hashtable初始容量為11,兩者的填滿因數預設都是0.75。HashMap擴充容量是當前容量翻倍即:capacity*2,Hashtable擴充容量是容量翻倍+1即:capacity*2+1(關於擴容和填滿因數後面會講)
4. 兩者的雜湊演算法不同,HashMap是先對key(鍵)求hashCode碼,然後再把這個碼值得高位和低位做&(與)運算,把源碼如下:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
i = (n - 1) & hash
然後把hash(key)返回的雜湊值與HashMap的初始容量(也叫初始數組的長度)減一做&(與運算)就可以計算出此索引值對應該儲存到數組的那個位置上(hash&(n-1))。這裡為什麼不用key本身的hashcode方法,而又是右移動16位又是異或操作。開發人員這樣做的目的是什麼呢。我的理解是當數組容量很小的時候,計算元素在數組中的位置(n-1)&hash,只用到了hash值的低位,這樣當不同的hash值低位相同,高位不同的時候會產生衝突。實際上的hash值將hashcode低16位與高16位做異或運算,相當於混合了高位和低位,增加了隨機性。當然是衝突越少越好,元素的分布越隨機越好。
而Hashtable計算位置的方式如下:
int hash = key.hashCode();int index = (hash & 0x7FFFFFFF) % tab.length;
直接計算key的雜湊碼,然後與2的31次方做&(與運算),然後對數組長度取餘數計算位置。
HashMap存取元素詳解
HashMap的儲存方式是雜湊表,那麼什麼是雜湊表,其實就是數組+鏈表。HashMap初始數組長度為16。數組的每個元素都儲存著鏈表頭的地址(或者為null),在向HashMap中put(key,value)的時候,先使用hash演算法計算雜湊值,然後再和數組的長度減一做與運算。計算出此索引值對應該儲存到數組的那個位置上,如果此位置沒有元素,意思就是鏈表的頭結點為null,那麼就建立一個node結點,把key,value以及next儲存。Node類源碼如下:
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } .... ...... ........}
這裡我只貼出了部分,看的出來,這個儲存索引值對的node結點是一個實現了Map.Entry的內部類,裡面的屬性有hash值(就是通過hash演算法計算出來的),key,value,以及next。當執行put(key,value)的時候,如果計算出來的數組位置上有元素的話(說明計算出的hash值和此數組位置對應的單鏈表上的所有元素的hash值相同,即發生衝突。),就沿著此數組位置對應的單鏈表上的結點一個個比較,如果遇到相同的key,就用新的value替換掉舊的value,如果找不到相同的key,就建立一個node結點,儲存hash值,key和value,然後插入到此單鏈表的尾部。插入之後,這裡程式會判斷此單鏈表上的結點個數(這裡注意,不是全部的元素結點個數,而是此單鏈表上的結點個數,和其他數組位置上的單鏈表無關)是否超過限制(HashMap預設是8),如果超過限制,那麼HashMap就會把此單鏈錶轉成紅/黑樹狀結構(關於什麼是紅/黑樹狀結構,請讀者自行百度),這樣做的目的是為了提高get(key)的速度。由時間複雜度原來的O(n)變成了O(logn)。到這還不算完,一旦插入新的node,程式就會檢查HashMap的裝載量(全部索引值對的個數)是否超過閾值,這個閾值是計算出來的,就是裝載因子乘上數組容量。一旦裝載量大於此閾值,程式就會執行resize()方法進行擴充容量,HashMap是直接擴容2倍,擴容之後,將原來鏈表數組的每一個鏈表分成奇偶兩個子鏈表分別掛在新鏈表數組的散列位置,這樣就減少了每個鏈表的長度,增加尋找效率,但是擴容是很費時的。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 預設初始數組容量 16static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量static final float DEFAULT_LOAD_FACTOR = 0.75f;//裝載因子預設0.75static final int TREEIFY_THRESHOLD = 8;//單鏈表結點個數超過8就轉成紅/黑樹狀結構int threshold; //閾值,超過此數,就進行擴充容量
下面看下HashMap儲存元素的大致情況:
現在看下put操作的源碼解析:
現在看下get操作的源碼解析:
其實,HashMap的初始容量和裝載因子都是可以通過HashMap的有參構造進行改的,如果使用無參的建構函式定義HashMap,那麼這兩個屬性都是預設的。至此,HashMap的底層實現原理就介紹完了,下面簡單說下Hashtable和ConcurrentHashMap。它們兩個都是安全執行緒的,都不可以儲存值為null的key,但是它們線上程同步上有些區別。Hashtable是在方法上使用synchronized關鍵字,其實這是對對象加鎖,鎖住的都是對象整體,當Hashtable的大小增加到一定的時候,效能會急劇下降,因為迭代時需要被鎖定很長的時間。ConcurrentHashMap算是對上述問題的最佳化。ConcurrentHashMap引入了分割(Segment),可以理解為它把一個大的Map拆分成N個小的Hashtable,在put方法中,會根據雜湊演算法來先決定具體存放進哪個Segment,如果我們查看Segment的put操作源碼,會發現內部使用的同步機制還是基於鎖的,但是這樣可以只對Map的一部分(Segment)進行上鎖,影響的只是將要放入同一個Segment的元素的put操作,保證同步的時候,鎖住的不是整個Map(而Hashtable是鎖住全部的),相對於Hashtable的synchronized關鍵字鎖的粒度更精細了一些,提高了多線程環境下的效能,所以Hashtable已被淘汰了。