HashMap實現原理

HashMap的原理及實現

  • 我的對HashMap的總結,有錯誤請留言.
  • 本文是純文字介紹的,若是有朋友喜歡結合代碼的話也能夠直接點擊文末連接。
  • 感謝閱讀.

概述

  • HashMap是在JDK1.2中引入的一種K/V對形式的集合類.
  • 在底層,HashMap經過數組和單鏈表組合的結構形式來存儲數據,數組在這做爲一個外部結構,數組中的每一個節點被稱作Bucket(桶),而桶是由在單鏈表構成,JDK1.8以後爲了解決長鏈表下,查詢和插入效率低下的狀況,又引入了紅黑樹的做爲桶的實現方式,
  • 桶中的各節點是由HashMap定義的Node內部類生成的,是普通的鏈表節點類.

HashMap的實現方式

  • 注意:HashMap是線程不安全的,在JDK1.8以前多線程狀況下甚至可能會出現環路(後面會講),因此多線程狀態下仍是要使用ConcurrentHashMap的.

重點參數

  • HashMap的參數很少,除去當作默認屬性的靜態常量和底層數組對象,就只有如下五個
transient Node<K,V>[] table;
transient int size
transient int modCount; 
int threshold;
final float loadFactor;
複製代碼
  • table就是整個HashMap的底層數組,table的初始化並不在構造函數中完成,而是在resize()方法中完成.html

    • table的初始化可能有點繞,構造函數中最多指定了閾值threshold和負載因子loadFactor並無容量相關,可是在resize()方法中會根據舊容量和舊閾值判斷新容量是等於默認容量,舊閾值或者兩倍舊容量,最後根據新容量建立新數組
  • loadFactor就是所謂的負載因子,默認爲0.75,是控制擴容時機的關鍵屬性,由於擴容發生在當前元素個數超過閾值時,而閾值等於當前容量乘以負載因子.java

  • modCount爲修改計數,是fast-fail機制的關鍵參數.在對Map中的元素作新增/刪除操做時會自增,但修改不會(putVal()方法中覆蓋原值)git

新增邏輯

  • HashMap的新增過程重點主要仍是定位,如何肯定元素在數組中的位置,HashMap採用的就是Hash算法
    1. 首先HashMap會根據Key的hash值,按照表達式(n - 1) & hash計算出桶的下標
    2. 若是此時桶爲空,會建立一個新的Node,做爲鏈表的第一個元素,直接存放在數組中.(之前還據說過什麼鏈表首節點爲空的狀況,是假的.)
    3. 若是節點存在又會區分樹節點(TreeNode)和普通節點(Node)兩種狀況.
      • 普通節點會直接從首節點往下遍歷找到尾節點,並將帶插入節點添加到末尾
      • 樹節點會調用,TreeNode的方法插入到樹中.
  • 另外新增前會判斷底層數組table是否初始化,新增後會判斷該桶大小是否超過的8,超過則轉化爲紅黑樹,再判斷整個數組是否須要擴容.
  • Hash同時也叫散列,能夠把任意長度的輸入經過算法,換算成固定長度的輸出,不一樣元素經過Hash算法得到的下標一致能夠被稱之爲衝突或者碰撞,Hash算法的要求就是使元素儘可能少的發生碰撞,從而均勻的散佈在數組中.而發生碰撞時,像HashMap這種以一個列表下掛的方式能夠被稱爲拉鍊法.

查找邏輯

  • 此處的查找邏輯是指調用get()方法,經過key值查找的狀況,若是本身遍歷的另說.
    1. 一樣是根據表達式(n - 1) & hash計算出桶的下標(能夠說是至關重要了),若獲得的桶爲空,直接返回null
    2. 不爲空時則會遍歷整個桶,並根據key.equals(k)判斷是否相等
    3. 遍歷的方法也會根據節點類型的不一樣而不一樣,可是區分節點前直接存放在數組中的頭結點是要先進行判斷的.感受上性能影響不大吧
  • 從查找的過程能夠看出,肯定桶下標的計算不存在隨機性,時間複雜度就爲O(1),具體的性能體如今遍歷這一塊,鏈表查詢的時間複雜度爲O(n),因此鏈表越長遍歷時間也就越長,插入和查找的效率也就越低.因此在JDK1.8以後引入的紅黑樹做爲桶的另外一種實現方法.當鏈表長度大於8時,桶的實現會轉化爲紅黑樹.
  • HashMap的性能很大一部分取決於Hash算法..

RESIZE邏輯

  • 經過插入和查找咱們能夠知道,在數組大小不變的狀況下,鏈表越長或者說樹的高度越高都會致使操做性能下降,因此此時頗有必要經過擴容數組的方式,從新排列桶中元素,下降鏈表長度,減小樹的高度.github

  • 首先,觸發擴容的狀況是size > threshold即元素個數大於閾值.整個擴容過程能夠簡單的拆分爲如下幾步:算法

    1. 對數組進行擴充,通常狀況下是數組容量和閾值都變爲原來的兩倍,此間會有上限判斷,容量最大爲1 << 30也就是2^30.
    2. 遍歷舊數組,從新判斷元素的位置並散佈到新數組.
  • resize()方法中從新散佈元素的方法仍是頗有意思的(除去單元素鏈表和紅黑樹(桶的容量在1~7之間)shell

    • 首先將新數組分爲兩部分lohi(源碼是loHead和hiHead,我猜是low和high,怎麼縮寫這麼隨意),lo表示0到舊容量大小部分,hi表示餘下算是新加入的部分,並以此建立兩個鏈表的節點
    • 根據表達式e.hash & oldCap判斷元素是否分佈在lo部分,是就掛到lo鏈表下面,否就掛到hi鏈表下面.
    • lo鏈表掛到和舊數組相同位置的桶,而hi則掛到下標爲原下標 + 舊數組容量的桶.
    • 此處的依據就是e.hash & (oldCap - 1) + oldCap == e.hash & (oldCap << 1) -1
  • 能夠看出resize()方法會調整所有的元素散列狀況,所以過於頻繁的resize會下降HashMap的性能,所以若是一開始能夠大概知道所須要存放的元素個數時,儘可能直接指定容量大小.數組

  • JDK1.7以前的resize()方法在併發條件下可能會發生閉環問題,但在JDK1.8以後不會在出現,但並不表明HashMap能夠在併發條件下使用了,小部分狀況仍是會出現數據丟失等問題.安全

  • 介紹JDK1.8以前的閉環問題詳情的文章多線程

  • HashMap的懶加載問題併發

    • 查看HashMap的源碼,你會發現底層數組table的建立其實並非在構造函數中完成的,而是resize()方法中,這就是所謂的懶加載,數組對象並不是是在一開始就建立的,而是在第一次插入操做以前完成的。

關於HashMap一些問題

擾動函數

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
複製代碼

擾動函數的邏輯很簡單就是hashCode的高16位和低16位異或.

擾動函數的做用就是增長散列的隨機性,使元素可以更均勻的分佈在數組中,減小衝突從而捎帶提升性能.

至於爲何,能夠看hash(*)用到的地方,hash(*)被用來計算元素的下標.而下標的計算公式以下

tab[i = (n - 1) & hash]   // n表示數組的長度
複製代碼

由於HashMap的容量必定會是2的次冪,因此減1以後轉化爲二進制會變爲一串0加一串1的,例如長度爲4時,減去1,就會變爲000…00011(前面30個0),再結合&能夠發現他只使用了hashCode的末尾幾位,高位是所有沒用.

而通過擾動函數,將高16位和低16位異或以後至關於高低位都用到了,其散列的隨機性也就增長了.

HashMap的容量爲何必定要是2的次冪

  • 容量爲2次冪有兩個優勢
    1. 在下標運算的時候使用(length - 1) & hash)代替hash % length,相對來講位運算性能更佳,速度更快。
    2. 而在採用(length - 1) & hash的方式計算下標以後,若是不是二次冪的容量,出現碰撞的概率將會大大增長,例如咱們取17做爲容量((17 -1) => 0001000),通過&與運算,能夠想象會有一大批的元素直接掛在0號桶。
  • 能夠說這是一整套的策略,若是使用hash & length的話,也不用要求容量必定是二次冪,但各方面的性能老是會差一點的。

HashMap和HashTable的區別

  • HashTable都沒用過了,但之前還稍微看過
  1. 最大的區別就是HashTable是線程安全的,暴力的加方法級synchronized.而HashMap是線程不安全的,併發狀況下可能會出現數據丟失等狀況.
  2. HashTable不容許null值,而HashMap容許null值.(包括key和value)
  3. HashCode的使用不一樣,HashTable是直接調用hashCode,而HashMap會通過擾動函數.並且HashMap中用&代替了%
  4. HashTable數組默認是11,且增加爲2n+1,而HashMap默認爲16,增加爲2n,且硬性要求長度爲2的次冪.
  5. HashTable並非和HashMap同樣繼承自AbstractMap的,它繼承自一個獨立的父類AbstractDictionary
  6. 還有就是遍歷方法的不一樣.瞭解不深先不說話.

  • 最後附上完整的源碼閱讀,蠻久以前寫的,不過被朋友吐槽說大段的代碼混着註釋實在看不下去,因此寫了這篇總結性的
相關文章
相關標籤/搜索