本文爲原創博文,轉載請註明出處,侵權必究!java
每一個java程序員都知道,HashMap是java中最重要的集合類之一,也是找工做面試中很是常見的考點,由於HashMap的實現自己確實蘊含了不少精妙的代碼設計。node
對於普通的程序員,可能僅僅能說出HashMap線程不安全,容許key、value爲null,以及不要求線程安全時,效率上比HashTable要快一些。稍微好一些的,會對具體實現有過大概瞭解,能說出HashMap由數組+鏈表+RBT實現,並瞭解HashMap的擴容機制。但若是你真的有一個刨根問題的熱情,那麼你確定會想知道具體是如何一步步實現的。HashMap的源碼一共2000多行,很難在這裏每一句都說明,但這篇文章會讓你透徹的理解到咱們平時經常使用的幾個操做下,HashMap是如何工做的。程序員
要先提一下的是,我看過不少講解HashMap原理的文章,有一些講的很是好,但這些文章習慣於把源代碼和邏輯分析分開,致使出現了大段的文字講解代碼,閱讀起來有些吃力和枯燥。因此我想嘗試另外一種風格,將更多的內容寫進註釋裏,可能看起來有些囉嗦,但對於一些新手的理解,應該會有好的效果。面試
首先是瞭解HashMap的幾個核心成員變量(如下均爲jdk源碼):數組
1 transient Node<K,V>[] table; //HashMap的哈希桶數組,很是重要的存儲結構,用於存放表示鍵值對數據的Node元素。 2 3 transient Set<Map.Entry<K,V>> entrySet; //HashMap將數據轉換成set的另外一種存儲形式,這個變量主要用於迭代功能。 4 5 transient int size; //HashMap中實際存在的Node數量,注意這個數量不等於table的長度,甚至可能大於它,由於在table的每一個節點上是一個鏈表(或RBT)結構,可能不止有一個Node元素存在。 6 7 transient int modCount; //HashMap的數據被修改的次數,這個變量用於迭代過程當中的Fail-Fast機制,其存在的意義在於保證發生了線程安全問題時,能及時的發現(操做前備份的count和當前modCount不相等)並拋出異常終止操做。 8 9 int threshold; //HashMap的擴容閾值,在HashMap中存儲的Node鍵值對超過這個數量時,自動擴容容量爲原來的二倍。 10 11 final float loadFactor; //HashMap的負載因子,可計算出當前table長度下的擴容閾值:threshold = loadFactor * table.length。
顯然,HashMap的底層實現是基於一個Node的數組,那麼Node是什麼呢?在HashMap的內部能夠看見定義了這樣一個內部類:安全
1 static class Node<K,V> implements Map.Entry<K,V> { 2 final int hash; 3 final K key; 4 V value; 5 Node<K,V> next; 6 7 Node(int hash, K key, V value, Node<K,V> next) { 8 this.hash = hash; 9 this.key = key; 10 this.value = value; 11 this.next = next; 12 } 13 14 public final K getKey() { return key; } 15 public final V getValue() { return value; } 16 public final String toString() { return key + "=" + value; } 17 18 public final int hashCode() { 19 return Objects.hashCode(key) ^ Objects.hashCode(value); 20 } 21 22 public final V setValue(V newValue) { 23 V oldValue = value; 24 value = newValue; 25 return oldValue; 26 } 27 28 public final boolean equals(Object o) { 29 if (o == this) 30 return true; 31 if (o instanceof Map.Entry) { 32 Map.Entry<?,?> e = (Map.Entry<?,?>)o; 33 if (Objects.equals(key, e.getKey()) && 34 Objects.equals(value, e.getValue())) 35 return true; 36 } 37 return false; 38 } 39 }
咱們大致看一下這個內部類就能夠知道,它實現了Map.Entry接口。其內部的變量含義也很明確,hash值、key\value對和實現鏈表和紅黑樹所須要的指針索引。數據結構
既然知道了HashMap的基本結構,那麼這些變量的默認值都是多少呢?咱們再看一下HashMap定義的一些常量:併發
1 //默認的初始容量爲16,必須是2的冪次 2 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
3 4 //最大容量即2的30次方 5 static final int MAXIMUM_CAPACITY = 1 << 30; 6 7 //默認加載因子 8 static final float DEFAULT_LOAD_FACTOR = 0.75f; 9 10 //當put一個元素時,其鏈表長度達到8時將鏈表轉換爲紅黑樹 11 static final int TREEIFY_THRESHOLD = 8; 12 13 //鏈表長度小於6時,解散紅黑樹 14 static final int UNTREEIFY_THRESHOLD = 6; 15 16 //默認的最小的擴容量64,爲避免從新擴容衝突,至少爲4 * TREEIFY_THRESHOLD=32,即默認初始容量的2倍 17 static final int MIN_TREEIFY_CAPACITY = 64;
以上就是咱們對HashMap的初步認識,下面進入正題,看看HashMap是如何添加、查找與刪除數據的。app
首先來看put方法,我儘可能在每行都加註釋闡明這一行的含義,讓閱讀起來更容易理解。函數
1 public V put(K key, V value) { 2 return putVal(hash(key), key, value, false, true); 3 } 4 5 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, //這裏onlyIfAbsent表示只有在該key對應原來的value爲null的時候才插入,也就是說若是value以前存在了,就不會被新put的元素覆蓋。 6 boolean evict) { //evict參數用於LinkedHashMap中的尾部操做,這裏沒有實際意義。 7 Node<K,V>[] tab; Node<K,V> p; int n, i; //定義變量tab是將要操做的Node數組引用,p表示tab上的某Node節點,n爲tab的長度,i爲tab的下標。 8 if ((tab = table) == null || (n = tab.length) == 0) //判斷當table爲null或者tab的長度爲0時,即table還沒有初始化,此時經過resize()方法獲得初始化的table。 9 n = (tab = resize()).length; //這種狀況是可能發生的,HashMap的註釋中提到:The table, initialized on first use, and resized as necessary。 10 if ((p = tab[i = (n - 1) & hash]) == null) //此處經過(n - 1) & hash 計算出的值做爲tab的下標i,並另p表示tab[i],也就是該鏈表第一個節點的位置。並判斷p是否爲null。 11 tab[i] = newNode(hash, key, value, null); //當p爲null時,代表tab[i]上沒有任何元素,那麼接下來就new第一個Node節點,調用newNode方法返回新節點賦值給tab[i]。 12 else { //下面進入p不爲null的狀況,有三種狀況:p爲鏈表節點;p爲紅黑樹節點;p是鏈表節點但長度爲臨界長度TREEIFY_THRESHOLD,再插入任何元素就要變成紅黑樹了。 13 Node<K,V> e; K k; //定義e引用即將插入的Node節點,而且下文能夠看出 k = p.key。 14 if (p.hash == hash && //HashMap中判斷key相同的條件是key的hash相同,而且符合equals方法。這裏判斷了p.key是否和插入的key相等,若是相等,則將p的引用賦給e。 15 ((k = p.key) == key || (key != null && key.equals(k)))) //這一步的判斷實際上是屬於一種特殊狀況,即HashMap中已經存在了key,因而插入操做就不須要了,只要把原來的value覆蓋就能夠了。 16 e = p; //這裏爲何要把p賦值給e,而不是直接覆蓋原值呢?答案很簡單,如今咱們只判斷了第一個節點,後面還可能出現key相同,因此須要在最後一併處理。 17 else if (p instanceof TreeNode) //如今開始了第一種狀況,p是紅黑樹節點,那麼確定插入後仍然是紅黑樹節點,因此咱們直接強制轉型p後調用TreeNode.putTreeVal方法,返回的引用賦給e。 18 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //你可能好奇,這裏怎麼不遍歷tree看看有沒有key相同的節點呢?其實,putTreeVal內部進行了遍歷,存在相同hash時返回被覆蓋的TreeNode,不然返回null。 19 else { //接下里就是p爲鏈表節點的情形,也就是上述說的另外兩類狀況:插入後仍是鏈表/插入後轉紅黑樹。另外,上行轉型代碼也說明了TreeNode是Node的一個子類。 20 for (int binCount = 0; ; ++binCount) { //咱們須要一個計數器來計算當前鏈表的元素個數,並遍歷鏈表,binCount就是這個計數器。 21 if ((e = p.next) == null) { //遍歷過程當中當發現p.next爲null時,說明鏈表到頭了,直接在p的後面插入新的鏈表節點,即把新節點的引用賦給p.next,插入操做就完成了。注意此時e賦給p。 22 p.next = newNode(hash, key, value, null); //最後一個參數爲新節點的next,這裏傳入null,保證了新節點繼續爲該鏈表的末端。 23 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //插入成功後,要判斷是否須要轉換爲紅黑樹,由於插入後鏈表長度加1,而binCount並不包含新節點,因此判斷時要將臨界閾值減1。 24 treeifyBin(tab, hash); //當新長度知足轉換條件時,調用treeifyBin方法,將該鏈表轉換爲紅黑樹。 25 break; //固然若是不知足轉換條件,那麼插入數據後結構也無需變更,全部插入操做也到此結束了,break退出便可。 26 } 27 if (e.hash == hash && //在遍歷鏈表的過程當中,我以前提到了,有可能遍歷到與插入的key相同的節點,此時只要將這個節點引用賦值給e,最後經過e去把新的value覆蓋掉就能夠了。 28 ((k = e.key) == key || (key != null && key.equals(k)))) //老樣子判斷當前遍歷的節點的key是否相同。 29 break; //找到了相同key的節點,那麼插入操做也不須要了,直接break退出循環進行最後的value覆蓋操做。 30 p = e; //在第21行我提到過,e是當前遍歷的節點p的下一個節點,p = e 就是依次遍歷鏈表的核心語句。每次循環時p都是下一個node節點。 31 } 32 } 33 if (e != null) { // existing mapping for key //左邊註釋爲jdk自帶註釋,說的很明白了,針對已經存在key的狀況作處理。 34 V oldValue = e.value; //定義oldValue,即原存在的節點e的value值。 35 if (!onlyIfAbsent || oldValue == null) //前面提到,onlyIfAbsent表示存在key相同時不作覆蓋處理,這裏做爲判斷條件,能夠看出當onlyIfAbsent爲false或者oldValue爲null時,進行覆蓋操做。 36 e.value = value; //覆蓋操做,將原節點e上的value設置爲插入的新value。 37 afterNodeAccess(e); //這個函數在hashmap中沒有任何操做,是個空函數,他存在主要是爲了linkedHashMap的一些後續處理工做。 38 return oldValue; //這裏頗有意思,他返回的是被覆蓋的oldValue。咱們在使用put方法時不多用他的返回值,甚至忘了它的存在,這裏咱們知道,他返回的是被覆蓋的oldValue。 39 } 40 } 41 ++modCount; //收尾工做,值得一提的是,對key相同而覆蓋oldValue的狀況,在前面已經return,不會執行這裏,因此那一類狀況不算數據結構變化,並不改變modCount值。 42 if (++size > threshold) //同理,覆蓋oldValue時顯然沒有新元素添加,除此以外都新增了一個元素,這裏++size並與threshold判斷是否達到了擴容標準。 43 resize(); //當HashMap中存在的node節點大於threshold時,hashmap進行擴容。 44 afterNodeInsertion(evict); //這裏與前面的afterNodeAccess同理,是用於linkedHashMap的尾部操做,HashMap中並沒有實際意義。1 45 return null; //最終,對於真正進行插入元素的狀況,put函數一概返回null。 46 }
在上述代碼中的第十行,HashMap根據 (n - 1) & hash 求出了元素在node數組的下標。這個操做很是精妙,下面咱們仔細分析一下計算下標的過程,主要分三個階段:計算hashcode、高位運算和取模運算。
首先,傳進來的hash值是由put方法中的hash(key)產生的(上述第2行),咱們來看一下hash()方法的源碼:
1 static final int hash(Object key) { 2 int h; 3 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 4 }
這裏經過key.hashCode()計算出key的哈希值,而後將哈希值h右移16位,再與原來的h作異或^運算——這一步是高位運算。設想一下,若是沒有高位運算,那麼hash值將是一個int型的32位數。而從2的-31次冪到2的31次冪之間,有將近幾十億的空間,若是咱們的HashMap的table有這麼長,內存早就爆了。因此這個散列值不能直接用來最終的取模運算,而須要先加入高位運算,將高16位和低16位的信息"融合"到一塊兒,也稱爲"擾動函數"。這樣才能保證hash值全部位的數值特徵都保存下來而沒有遺漏,從而使映射結果儘量的鬆散。最後,根據 n-1 作與操做的取模運算。這裏也能看出爲何HashMap要限制table的長度爲2的n次冪,由於這樣,n-1能夠保證二進制展現形式是(以16爲例)0000 0000 0000 0000 0000 0000 0000 1111。在作"與"操做時,就等同於截取hash二進制值得後四位數據做爲下標。這裏也能夠看出"擾動函數"的重要性了,若是高位不參與運算,那麼高16位的hash特徵幾乎永遠得不到展示,發生hash碰撞的概率就會增大,從而影響性能。
HashMap的put方法的源碼實現就是這樣了,整理思路很是連貫。這裏面有幾個函數的源碼(好比resize、putTreeValue、newNode、treeifyBin)限於篇幅緣由,就不貼了,後面應該還會更新在其餘博客裏,有興趣的同窗也能夠本身挖掘一下。
讀完了put的源碼,其實已經能夠很清晰的理清HashMap的工做原理了。接下來再看get方法的源碼,就很是的簡單:
1 public V get(Object key) { 2 Node<K,V> e; 3 return (e = getNode(hash(key), key)) == null ? null : e.value; //根據key及其hash值查詢node節點,若是存在,則返回該節點的value值。 4 } 5 6 final Node<K,V> getNode(int hash, Object key) { //根據key搜索節點的方法。記住判斷key相等的條件:hash值相同 而且 符合equals方法。 7 Node<K,V>[] tab; Node<K,V> first, e; int n; K k; 8 if ((tab = table) != null && (n = tab.length) > 0 && //根據輸入的hash值,能夠直接計算出對應的下標(n - 1)& hash,縮小查詢範圍,若是存在結果,則一定在table的這個位置上。 9 (first = tab[(n - 1) & hash]) != null) { 10 if (first.hash == hash && // always check first node 11 ((k = first.key) == key || (key != null && key.equals(k)))) //判斷第一個存在的節點的key是否和查詢的key相等。若是相等,直接返回該節點。 12 return first; 13 if ((e = first.next) != null) { //遍歷該鏈表/紅黑樹直到next爲null。 14 if (first instanceof TreeNode) //當這個table節點上存儲的是紅黑樹結構時,在根節點first上調用getTreeNode方法,在內部遍歷紅黑樹節點,查看是否有匹配的TreeNode。 15 return ((TreeNode<K,V>)first).getTreeNode(hash, key); 16 do { 17 if (e.hash == hash && //當這個table節點上存儲的是鏈表結構時,用跟第11行一樣的方式去判斷key是否相同。 18 ((k = e.key) == key || (key != null && key.equals(k)))) 19 return e; 20 } while ((e = e.next) != null); //若是key不一樣,一直遍歷下去直到鏈表盡頭,e.next == null。 21 } 22 } 23 return null; 24 }
由於查詢過程不涉及到HashMap的結構變更,因此get方法的源碼顯得很簡潔。核心邏輯就是遍歷table某特定位置上的全部節點,分別與key進行比較看是否相等。
以上即是HashMap最經常使用API的源碼分析,除此以外,HashMap還有一些知識須要重點學習:擴容機制、併發安全問題、內部紅黑樹的實現。這些內容我也會在以後陸續發文分析,但願能夠幫讀者完全理解HashMap的原理。