HashMap底層實現原理(JDK1.8)源碼分析

ref:http://www.javashuo.com/article/p-unpusnlq-dy.htmlhtml

     http://www.cnblogs.com/xiaolovewei/p/7993440.htmljava

  在JDK1.6,JDK1.7中,HashMap採用位桶+鏈表實現,即便用鏈表處理衝突,同一hash值的鏈表都存儲在一個鏈表裏。可是當位於一個桶中的元素較多,即hash值相等的元素較多時,經過node

key值依次查找的效率較低。而JDK1.8中,HashMap採用位桶+鏈表+紅黑樹實現,當鏈表長度超過閾值(8)時,將鏈表轉換爲紅黑樹,這樣大大減小了查找時間。算法

  簡單說下HashMap的實現原理:數組

  首先有一個每一個元素都是鏈表(可能表述不許確)的數組,當添加一個元素(key-value)時,就首先計算元素key的hash值,以此肯定插入數組中的位置,可是可能存在同一hash數據結構

的元素已經被放在數組同一位置了,這時就添加到同一hash值的元素的後面,他們在數組的同一位置,可是造成了鏈表,同一各鏈表上的Hash值是相同的,因此說數組存放的是鏈app

表。而當鏈表長度太長時,鏈表就轉換爲紅黑樹,這樣大大提升了查找的效率。函數

  當鏈表數組的容量超過初始容量的0.75時,再散列將鏈表數組擴大2倍,把原鏈表數組的搬移到新的數組中。性能

  即HashMap的原理圖是:優化

  

   

1、JDK1.8中的涉及到的數據結構

  一、位桶數組   

  複製代碼

   /**
       * 哈希表的結構是桶(數組)+單向鏈表+紅黑樹。
       * 因爲不一樣的key計算出的hash值可能相同,會形成hash衝突,引入單鏈表解決hash衝突。
       * 因此,桶中的各個元素hash值雖然相同,可是key不相同,由於hashmap不容許相同的key。
       * 當鏈表長度過大時,訪問速度降低,引入紅黑樹解決這一問題
       * 哈希表的容量(桶的個數)是2的倍數
       * 該數組的初始化放在了put中,構造函數中並無對其進行初始化
       */
      transient Node<k,v>[] table;//存儲(位桶)的數組</k,v>

  二、數組元素Node<K,V>實現了Entry接口

  複製代碼  

    1. //Node是單向鏈表,它實現了Map.Entry接口  
    2. static class Node<k,v> implements Map.Entry<k,v> {  
    3.     final int hash;  
    4.     final K key;  
    5.     V value;  
    6.     Node<k,v> next;  
    7.     //構造函數Hash值 鍵 值 下一個節點  
    8.     Node(int hash, K key, V value, Node<k,v> next) {  
    9.         this.hash = hash;  
    10.         this.key = key;  
    11.         this.value = value;  
    12.         this.next = next;  
    13.     }  
    14.    
    15.     public final K getKey()        { return key; }  
    16.     public final V getValue()      { return value; }  
    17.     public final String toString() { return key + = + value; }  
    18.    
    19.     public final int hashCode() {  
    20.         return Objects.hashCode(key) ^ Objects.hashCode(value);  
    21.     }  
    22.    
    23.     public final V setValue(V newValue) {  
    24.         V oldValue = value;  
    25.         value = newValue;  
    26.         return oldValue;  
    27.     }  
    28.     //判斷兩個node是否相等,若key和value都相等,返回true。能夠與自身比較爲true  
    29.     public final boolean equals(Object o) {  
    30.         if (o == this)  
    31.             return true;  
    32.         if (o instanceof Map.Entry) {  
    33.             Map.Entry<!--?,?--> e = (Map.Entry<!--?,?-->)o;  
    34.             if (Objects.equals(key, e.getKey()) &&  
    35.                 Objects.equals(value, e.getValue()))  
    36.                 return true;  
    37.         }  
    38.         return false;  
    39.     }  

  複製代碼

  三、紅黑樹

  複製代碼  

    1. //紅黑樹  
    2. static final class TreeNode<k,v> extends LinkedHashMap.Entry<k,v> {  
    3.     TreeNode<k,v> parent;  // 父節點  
    4.     TreeNode<k,v> left; //左子樹  
    5.     TreeNode<k,v> right;//右子樹  
    6.     TreeNode<k,v> prev;    // needed to unlink next upon deletion  
    7.     boolean red;    //顏色屬性  
    8.     TreeNode(int hash, K key, V val, Node<k,v> next) {  
    9.         super(hash, key, val, next);  
    10.     }  
    11.    
    12.     //返回當前節點的根節點  
    13.     final TreeNode<k,v> root() {  
    14.         for (TreeNode<k,v> r = this, p;;) {  
    15.             if ((p = r.parent) == null)  
    16.                 return r;  
    17.             r = p;  
    18.         }  
    19.     }  

  複製代碼

2、源碼中的數據域

  加載因子(默認0.75):爲何須要使用加載因子,爲何須要擴容呢?由於若是填充比很大,說明利用的空間不少,若是一直不進行擴容的話,鏈表就會愈來愈長,這樣查找的效率很低,

由於鏈表的長度很大(固然最新版本使用了紅黑樹後會改進不少),擴容以後,將原來鏈表數組的每個鏈表分紅奇偶兩個子鏈表分別掛在新鏈表數組的散列位置,這樣就減小了每一個鏈表的長

度,增長查找效率。

  HashMap原本是以空間換時間,因此填充比不必太大。可是填充比過小又會致使空間浪費。若是關注內存,填充比能夠稍大,若是主要關注查找性能,填充比能夠稍小。

  先來看看hashmap中的幾個參數:

  複製代碼 

  1. public class HashMap<k,v> extends AbstractMap<k,v> implements Map<k,v>, Cloneable, Serializable {  
  2.     private static final long serialVersionUID = 362498820763181265L;  
  3.     static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默認的初始化容量(桶的個數),是16, 要爲2的冪次  
  4.     static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量  
  5.     static final float DEFAULT_LOAD_FACTOR = 0.75f;//(填充比) 加載因子。當hash表中桶的數目超過當前容量與加載因子的乘積時,就會擴容 
  6.     //當add一個元素到某個位桶,其鏈表長度達到8時將鏈表轉換爲紅黑樹  
  7.     static final int TREEIFY_THRESHOLD = 8;  
  8.     static final int UNTREEIFY_THRESHOLD = 6;  //一個桶的鏈表還原閾值。當桶中元素個數小於這個值時,紅黑樹會還原爲鏈表。
  9.     static final int MIN_TREEIFY_CAPACITY = 64;  //哈希表的最小樹形化的容量。只有當表中的桶的個數(容量)大於這個值時,表中的桶才能樹形化(轉化成紅黑樹),不然,當桶內元素太多時,不是轉換成紅黑樹,而是擴容,由於容量不夠大。
  10.     transient Node<k,v>[] table;//存儲元素的數組  
  11.     transient Set<map.entry<k,v>> entrySet;  
  12.     transient int size;//存放元素的個數  
  13.     transient int modCount;//被修改的次數fast-fail機制  
  14.     int threshold;//臨界值 當實際大小(容量*填充比)超過臨界值時,會進行擴容   
  15.     final float loadFactor;//填充比(......後面略) 

  複製代碼

  這幾個參數的含義上面都有標註。HashMap默認的容量(桶的個數)是16,。默認的加載因子是0.75,加載因子loadFactor是影響hashMap進行擴容的指標之一,還有一個是容量,也就是table

數組的大小(桶的個數)。threshold是進行擴容的門限值,爲capacity*loadFactor 。

  當一個桶中元素個數大於8時(添加元素時判斷),會將鏈表轉成紅黑樹;當樹的節點個數小於6時(刪除節點時判斷),會轉成鏈表。

  MIN_TREEIFY_CAPACITY變量:最小樹形化的值。意思是:當桶的個數沒有達到這個值(64)時,即便桶中元素個數大於8時,也不會轉成紅黑樹,而是直接擴容(resize(),該方法後面介

紹),擴大桶的個數。只有當桶的個數大於等於該值時,纔會樹形化。

3、HashMap的構造函數

  HashMap的構造方法有4種,主要涉及到的參數有:指定初始容量、指定填充比和用來初始化的Map

  複製代碼 

  1. //構造函數1  
  2. public HashMap(int initialCapacity, float loadFactor) {  
  3.     //指定的初始容量非負  
  4.     if (initialCapacity < 0)  
  5.         throw new IllegalArgumentException(Illegal initial capacity:  +  
  6.                                            initialCapacity);  
  7.     //若是指定的初始容量大於最大容量,置爲最大容量  
  8.     if (initialCapacity > MAXIMUM_CAPACITY)  
  9.         initialCapacity = MAXIMUM_CAPACITY;  
  10.     //填充比爲正  
  11.     if (loadFactor <= 0 || Float.isNaN(loadFactor))  
  12.         throw new IllegalArgumentException(Illegal load factor:  +  
  13.                                            loadFactor);  
  14.     this.loadFactor = loadFactor;  
  15.     this.threshold = tableSizeFor(initialCapacity);//新的擴容臨界值  
  16. }  
  17.    
  18. //構造函數2  
  19. public HashMap(int initialCapacity) {  
  20.     this(initialCapacity, DEFAULT_LOAD_FACTOR);  
  21. }  
  22.    
  23. //構造函數3  
  24. public HashMap() {  
  25.     this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted  
  26. }  
  27.    
  28. //構造函數4用m的元素初始化散列映射  
  29. public HashMap(Map<!--? extends K, ? extends V--> m) {  
  30.     this.loadFactor = DEFAULT_LOAD_FACTOR;  
  31.     putMapEntries(m, false);  
  32. }  

  複製代碼

 

  從構造函數能夠看出,構造函數並無對底層的table數組進行初始化,而是和ArrayList同樣,將初始化數組的過程推遲到第一次添加元素時進行。第一個構造函數,將傳入的容量賦值給了

threshold門限,後面會在resize方法中根據該門限進行初始化。

4、HashMap的存取機制

  一、HashMap如何getValue值,看源碼

  複製代碼

  /**
     * 返回key對應的value
     * 1.計算key的hash值。
     * 2.根據hash值找到對應桶的第一個節點。
     * 3.判斷第一個節點是否是(比較hash值和key)。
     * 4.第一個節點不是就分成黑樹和鏈表繼續遍歷
     */
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    /**
     * 根據hash值和key找到對應的節點
     * 1.根據hash值找到對應的桶的第一個節點。若是第一個節點hash值以及key都對應的相等,則返回第一個。
     * 2.日後遍歷,看看是否是樹,而後遍歷。
     * 這裏找桶的算法是(n-1)&hash。n是桶的個數(2的冪次)。
     *
     * 這裏取模時,先後兩個數位置無所謂,只要有一個是2的冪次(b),另一個是a,
     * 取模就等價於求餘數(a%(b-1),(2的冪次-1)作分母)。
     * 因此,這裏是等價於hash%(n-1)。也就是找桶的位置,從0開始
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

  步驟簡述:

  1.計算key的hash值。

  2.根據hash值找到對應桶的第一個節點,hash&(n-1)。

  3.判斷第一個節點是否是(比較hash值和key)。

  4.第一個節點不是就分成黑樹和鏈表繼續遍歷

  這裏根據hash值找到對應桶的算法是(n-1)&hash。這個算法其實就是取模運算,又由於n是桶的個數,是2的冪次,因此該算法等價於hash%(n-1),也就是找到對應的桶的位置(從0開

始)。

  二、HashMap如何put(key,value),看源碼

  複製代碼

  /**
     * 存元素,見下個方法
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * 存元素的步驟:
     * 1.根據key計算hash值;
     * 2.判斷是不是第一次加入元素(table是否爲空),若是是,則調用resize函數初始化:
     *    若是threshold=0,則初始化爲16,;若是threshold不爲0(構造函數中傳入加載因子,會給threshold賦值,可是沒有初始化table)
     * 3.根據hash值找到((n-1)&hash)對應桶的第一個元素;若是第一個元素爲空,那麼直接插入新節點。
     * 4.若是第一個元素不爲空,則判斷結構是否是紅黑樹,若是是紅黑樹則調用紅黑樹插入的方法;
     * 5.若是不是紅黑樹,則依次遍歷鏈表,若是鏈表有和傳入的key相同的key,則用新的value替換原來的value,並返回舊value;
     * 6.若是沒有相同的key,則插入到鏈表的最後。並判斷新鏈表的大小是否超過門限,超過則轉換成紅黑樹。
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        /**
         * 構造函數中沒有對table進行初始化,因此第一次put時,會進行判斷table是否爲空,爲空則要進行初始化。
         * 也就是table初始化爲初始化容量16.
         * resize()函數就是擴容:table爲空時,擴容(也就是初始化)爲默認容量16;table不爲空時,擴容兩倍(知足容量是2的冪次)
         */
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //根據hash值,找到對應桶位置的第一個元素,若是該元素爲空,則直接插入。
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //若是第一個元素不爲空,且該元素的key與傳入的key同樣,說明已經存在該key,記錄下來
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    //若是是最後一個了,且key都不相同,就將新節點插入到鏈表最後
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //若是新加入節點後,鏈表大小超過閾值8,就轉成紅黑樹
                        if (binCount >= TREEIFY_THRESHOLD - 1) // 由於從-1開始的。也就是-1到7,也就是大於8個節點時
                            treeifyBin(tab, hash);
                        break;
                    }//若是有相同的key,跳出循環
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            /**
             * 若是有相同的key,將用新的value替換就的value,並返回原來的value
             */
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;//新換舊
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
複製代碼

  *存元素的步驟:

  1.根據key計算hash值;

  2.判斷是不是第一次加入元素(table是否爲空),若是是,則調用resize函數初始化(擴容):(見下面resize)

   若是threshold=0,則初始化爲16,;若是threshold不爲0,初始化爲threshold(構造函數中傳入加載因子,會給threshold賦值,可是沒有初始化table)

  3.根據hash值找到((n-1)&hash)對應桶的第一個元素;若是第一個元素爲空,那麼直接插入新節點。

  4.若是第一個元素不爲空,則判斷結構是否是紅黑樹,若是是紅黑樹則調用紅黑樹插入的方法;

  5.若是不是紅黑樹,則依次遍歷鏈表,若是鏈表有和傳入的key相同的key,則用新的value替換原來的value,並返回舊value;

  6.若是沒有相同的key,則插入到鏈表的最後。並判斷新鏈表的大小是否超過門限,超過則轉換成紅黑樹。

  7.判斷新size是否是大於threshold,是就擴容

5、HasMap的擴容機制resize()

  構造hash表時,若是不指明初始大小,默認大小爲16(即Node數組大小16),若是Node[]數組中的元素達到(填充比*Node.length)從新調整HashMap大小 變爲原來2倍大小,擴容很耗時。

  複製代碼

  /**
     * table爲空時,擴容(也就是初始化)爲默認容量16;table不爲空時,擴容兩倍(知足容量是2的冪次)
     * resize函數中新建一個散列表數組,容量爲舊錶的2倍,接着把舊錶的鍵值對遷移到新表(從新計算hash值,存入新表),
     * 這裏分三種狀況:
     1. 表項只有一個鍵值對時,針對新表計算新的桶位置並插入鍵值對
     2. 表項節點是紅黑樹節點時(說明這個bin元素較多已經轉成紅黑樹了),split這個bin。
     3. 表項節點包含多個鍵值對組成的鏈表時(拉鍊法),把鏈表上的鍵值對按hash值分紅兩串,一串放到新表的原索引位置,
        另一串放到新表的原索引位置+oldCap  處。

     */
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            //若是原來桶的個數已經最大了,就不擴容了,直接返回原來數組
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //若是原來的容量比默認容量大,且它的2倍也沒有超過最大容量,那麼新容量爲兩倍,新擴容門限也爲原來兩倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }//若是原來門限大於0,則新容量爲原來門限
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;//放到新表的原索引位置
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;//另外一串放到新表的 原索引+原容量  位置。
                        }
                    }
                }
            }
        }
        return newTab;
    }

  步驟:

  table不爲空時(原來容量>0),擴容兩倍(知足容量是2的冪次);

  當table爲空時:1,若是threshold>0,這就是構造函數中傳進來的初始化容量,初始化爲該容量threshold;2,threshold=0,沒有傳入初始化容量,初始化爲默認容量16.

  擴容兩倍步驟:

  * resize函數中新建一個散列表數組,容量爲舊錶的2倍,接着把舊錶的鍵值對遷移到新表(從新計算hash值,存入新表),

  * 這裏分三種狀況:遍歷每一個桶 j,

  1. 桶中只有一個鍵值對時,針對新表計算新的桶位置並插入鍵值對

  2. 桶中節點是紅黑樹節點時(說明這個bin元素較多已經轉成紅黑樹了),split這個bin。

  3. 桶中節點包含多個鍵值對組成的鏈表時(拉鍊法),把鏈表上的鍵值對按hash值分紅兩串(根據(hash & oldCap) == 0),一串放到新表的原索引位置 j ,另一串放到新表的 原索引位置

j+原表容量oldCap 處。

6、JDK1.8使用紅黑樹的改進

  在java jdk8中對HashMap的源碼進行了優化,在jdk7中,HashMap處理「碰撞」的時候,都是採用鏈表來存儲,當碰撞的結點不少時,查詢時間是O(n)。

  在jdk8中,HashMap處理「碰撞」增長了紅黑樹這種數據結構,當碰撞結點較少時,採用鏈表存儲,當較大時(>8個),採用紅黑樹(特色是查詢時間是O(logn))存儲(有一個閥值控制,大

於閥值(8個),將鏈表存儲轉換成紅黑樹存儲)。

   

  問題分析:

  你可能還知道哈希碰撞會對hashMap的性能帶來災難性的影響。若是多個hashCode()的值落到同一個桶內的時候,這些值是存儲到一個鏈表中的。最壞的狀況下,全部的key都映射到同一個

桶中,這樣hashmap就退化成了一個鏈表——查找時間從O(1)到O(n)。

  隨着HashMap的大小的增加,get()方法的開銷也愈來愈大。因爲全部的記錄都在同一個桶裏的超長鏈表內,平均查詢一條記錄就須要遍歷一半的列表。

  JDK1.8HashMap的紅黑樹是這樣解決的:

         若是某個桶中的記錄過大的話(當前是TREEIFY_THRESHOLD = 8),HashMap會動態的使用一個專門的treemap實現來替換掉它。這樣作的結果會更好,是O(logn),而不是糟糕的O(n)。

        它是如何工做的?前面產生衝突的那些KEY對應的記錄只是簡單的追加到一個鏈表後面,這些記錄只能經過遍從來進行查找。可是超過這個閾值後HashMap開始將列表升級成一個二叉樹,使

用哈希值做爲樹的分支變量,若是兩個哈希值不等,但指向同一個桶的話,較大的那個會插入到右子樹裏。若是哈希值相等,HashMap但願key值最好是實現了Comparable接口的,這樣它能夠按

照順序來進行插入。這對HashMap的key來講並非必須的,不過若是實現了固然最好。若是沒有實現這個接口,在出現嚴重的哈希碰撞的時候,你就並別期望能得到性能提高了。 

相關文章
相關標籤/搜索