HashMap實現原理及源碼分析(jdk1.8)

HashMap底層由數組+鏈表+紅黑樹組成,可接受null值,非線程安全node

  • 一、基本屬性
 
 
transient Node<K,V>[] table; //hashmap數組
 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默認容量16

    static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量

    static final float DEFAULT_LOAD_FACTOR = 0.75f;//默認負載因子

    static final int TREEIFY_THRESHOLD = 8; //鏈表節點轉換紅黑樹節點的閾值

    static final int UNTREEIFY_THRESHOLD = 6; //紅黑樹節點轉換鏈表節點的閾值

    static final int MIN_TREEIFY_CAPACITY = 64;// 轉紅黑樹時, table的最小長度 // 基本hash節點, 繼承自Entry
    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; } public final K getKey()        { return key; } public final V getValue()      { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } } //構造函數
    public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } //構造函數
    public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } //構造函數
    public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; } // 紅黑樹節點
    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red; TreeNode(int hash, K key, V val, Node<K,V> next) { super(hash, key, val, next); } /** * Returns root of tree containing this node. */
        final TreeNode<K,V> root() { for (TreeNode<K,V> r = this, p;;) { if ((p = r.parent) == null) return r; r = p; } } //...
    }
  • 二、hash算法

HashMap定位數組索引位置,直接決定了hash方法的離散性能。下面是定位哈希桶數組的源碼:算法

static final int hash(Object key) { // 計算key的hash值
        int h; // 1.先拿到key的hashCode值; 2.將hashCode的高16位參與運算
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } // 將(tab.length - 1) 與 hash值進行&運算
    int index = (tab.length - 1) & hash;

HashMap底層數組的長度老是2的n次方,而且取模運算爲「h mod table.length」,對應上面的公式,能夠獲得該運算等同於「h & (table.length - 1)」。這是HashMap在速度上的優化,由於&比%具備更高的效率。數組

在JDK1.8的實現中,還優化了高位運算的算法,將hashCode的高16位與hashCode進行異或運算,主要是爲了在table的length較小的時候,讓高位也參與運算,而且不會有太大的開銷。安全

  • 三、get方法
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; // table不爲空 && table長度大於0 && table索引位置(根據hash值計算出)節點不爲空
        if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // first的key等於傳入的key則返回first對象
            if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k)))) return first; //first的key不等於傳入的key則說明是鏈表,向下遍歷
            if ((e = first.next) != null) { // 判斷是否爲TreeNode,是則爲紅黑樹 // 若是是紅黑樹節點,則調用紅黑樹的查找目標節點方法getTreeNode
                if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); //走下列步驟表示是鏈表,循環至節點的key與傳入的key值相等
                do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } }
//找不到符合的返回空
return null; }
  • 四、put方法
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } 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是否爲空或者length等於0, 若是是則調用resize方法進行初始化
        if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 經過hash值計算索引位置, 若是table表該索引位置節點爲空則新增一個
        if ((p = tab[i = (n - 1) & hash]) == null) // 將索引位置的頭節點賦值給p
            tab[i] = newNode(hash, key, value, null); // table表該索引位置不爲空
        else { //判斷p節點的hash值和key值是否跟傳入的hash值和key值相等
            Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 若是相等, 則p節點即爲要查找的目標節點,賦值給e // 判斷p節點是否爲TreeNode, 若是是則調用紅黑樹的putTreeVal方法查找目標節點
            else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 走到這表明p節點爲普通鏈表節點
            else { // 遍歷此鏈表, binCount用於統計節點數
                for (int binCount = 0; ; ++binCount) { //p.next爲空表明目標節點不存在
                    if ((e = p.next) == null) { //新增一個節點插入鏈表尾部
                        p.next = newNode(hash, key, value, null); //若是節點數目超過8個,調用treeifyBin方法將該鏈表轉換爲紅黑樹
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
 treeifyBin(tab, hash); break; } //e節點的hash值和key值都與傳入的相等, 則e即爲目標節點,跳出循環
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // e不爲空則表明根據傳入的hash值和key值查找到了節點,將該節點的value覆蓋,返回oldValue
            if (e != null) { // existing mapping for key
                V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); // 用於LinkedHashMap
                return oldValue; } } //map修改次數加1
        ++modCount; //map節點數加1,若是超過閥值,則擴容
        if (++size > threshold) resize(); afterNodeInsertion(evict); // 用於LinkedHashMap
        return null; }

從上面的源碼分析能夠看出數據結構

一、若是節點已經存在,則更新原值app

二、若是節點不存在,則插入數組中,若是數組已經有值,則判斷是非是紅黑樹,若是是,則調用紅黑樹方法插入函數

三、若是插入的是鏈表,插入尾部,而後判斷節點數是否超過8,若是超過,則轉換爲紅黑樹源碼分析

四、先插入的數據,後面判斷是否超過閥值再進行的擴容性能

 

putTreeVal,插入紅黑樹方法就不看了,看下treeifyBin方法,該方法是將鏈表轉化爲紅黑樹,優化

final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; // table爲空或者table的長度小於64, 進行擴容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); // 根據hash值計算索引值, 遍歷該索引位置的鏈表
    else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); // 鏈表節點轉紅黑樹節點
            if (tl == null)    // tl爲空表明爲第一次循環
                hd = p; // 頭結點
            else { p.prev = tl;    // 當前節點的prev屬性設爲上一個節點
                tl.next = p;    // 上一個節點的next屬性設置爲當前節點
 } tl = p; // tl賦值爲p, 在下一次循環中做爲上一個節點
        } while ((e = e.next) != null);    // e指向下一個節點 // 將table該索引位置賦值爲新轉的TreeNode的頭節點
        if ((tab[index] = hd) != null) hd.treeify(tab); // 以頭結點爲根結點, 構建紅黑樹
    }
}

能夠看到,會先判斷tab的節點數是否超過64,若是沒超過,則進行擴容,若是超過了纔會轉換爲紅黑樹

能夠獲得兩個結論

一、何時轉換爲紅黑樹

當鏈表數目超過8,而且map節點數量超過64,纔會轉換爲紅黑樹

二、何時擴容(前提是map數目沒有超過最大容量值  1<<30 )

新增節點時,發生了碰撞,而且節點數目超過閥值

新增節點時,發生了碰撞,節點數量木有超過閥值,可是鏈表數目>8,map節點<64時

 

再看下resize()方法

final Node<K,V>[] resize() { //oldTab保存未擴容的tab
        Node<K,V>[] oldTab = table; //oldTab最大容量
        int oldCap = (oldTab == null) ? 0 : oldTab.length; //oldTab閥值
        int oldThr = threshold; int newCap, newThr = 0; //若是老map有值
        if (oldCap > 0) { // 老table的容量超過最大容量值,設置閾值爲Integer.MAX_VALUE,返回老表
            if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; //老table的容量沒有超過最大容量值,將新容量賦值爲老容量*2,若是新容量<最大容量而且老容量>=16, 則將新閾值設置爲原來的兩倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold
 } else if (oldThr > 0) // 老表的容量爲0, 老表的閾值大於0, 是由於初始容量被放入閾值
            newCap = oldThr;    // 則將新表的容量設置爲老表的閾值
        else {   //老表的容量爲0, 老表的閾值爲0, 則爲空表,設置默認容量和閾值
            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) { // 將索引值爲j的老表頭節點賦值給e
                    oldTab[j] = null; //將老表的節點設置爲空, 以便垃圾收集器回收空間 // 若是e.next爲空, 則表明老表的該位置只有1個節點, // 經過hash值計算新表的索引位置, 直接將該節點放在該位置
                    if (e.next == null) //                         newTab[e.hash & (newCap - 1)] = e; //e.next不爲空,判斷是不是紅黑樹
                    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; //若是e的hash值與老表的容量進行與運算爲0,則擴容後的索引位置跟老表的索引位置同樣
                            if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } //若是e的hash值與老表的容量進行與運算爲1,則擴容後的索引位置爲:老表的索引位置+oldCap
                            else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; // 最後一個節點的next設爲空
                            newTab[j] = loHead; // 將原索引位置的節點設置爲對應的頭結點
 } if (hiTail != null) { hiTail.next = null; // 最後一個節點的next設爲空
                            newTab[j + oldCap] = hiHead; // 將索引位置爲原索引+oldCap的節點設置爲對應的頭結點
 } } } } } return newTab; }

能夠看出,擴容時,節點重hash只分布在原索引位置與原索引+oldCap位置,爲何呢

假設老表的容量爲16,即oldCap=16,則新表容量爲16*2=32,假設節點1的hash值爲0000 0000 0000 0000 0000 1111 0000 1010,節點2的hash值爲0000 0000 0000 0000 0000 1111 0001 1010,則節點1和節點2在老表的索引位置計算以下圖計算1,因爲老表的長度限制,節點1和節點2的索引位置只取決於節點hash值的最後4位。再看計算2,計算2爲新表的索引計算,能夠知道若是兩個節點在老表的索引位置相同,則新表的索引位置只取決於節點hash值倒數第5位的值,而此位置的值恰好爲老表的容量值16,此時節點在新表的索引位置只有兩種狀況:原索引位置和原索引+oldCap位置(在此例中即爲10和10+16=26)。因爲結果只取決於節點hash值的倒數第5位,而此位置的值恰好爲老表的容量值16,所以此時新表的索引位置的計算能夠替換爲計算3,直接使用節點的hash值與老表的容量16進行位於運算,若是結果爲0則該節點在新表的索引位置爲原索引位置,不然該節點在新表的索引位置爲原索引+oldCap位置。

 

  • 五、remove()方法
public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value; } final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; // 若是table不爲空而且根據hash值計算出來的索引位置不爲空, 將該位置的節點賦值給p
    if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v; // 若是p的hash值和key都與入參的相同, 則p即爲目標節點, 賦值給node
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) {    // 不然向下遍歷節點
            if (p instanceof TreeNode)  // 若是p是TreeNode則調用紅黑樹的方法查找節點
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { do {    // 遍歷鏈表查找符合條件的節點 // 當節點的hash值和key與傳入的相同,則該節點即爲目標節點
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e;    // 賦值給node, 並跳出循環
                        break; } p = e;  // p節點賦值爲本次結束的e
                } while ((e = e.next) != null); // 指向像一個節點
 } } // 若是node不爲空(即根據傳入key和hash值查找到目標節點),則進行移除操做
        if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode)   // 若是是TreeNode則調用紅黑樹的移除方法
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); // 走到這表明節點是普通鏈表節點 // 若是node是該索引位置的頭結點則直接將該索引位置的值賦值爲node的next節點
            else if (node == p) tab[index] = node.next; // 不然將node的上一個節點的next屬性設置爲node的next節點, // 即將node節點移除, 將node的上下節點進行關聯(鏈表的移除) 
            else p.next = node.next; ++modCount; // 修改次數+1
            --size; // table的總節點數-1
            afterNodeRemoval(node); // 供LinkedHashMap使用
            return node;    // 返回被移除的節點
 } } return null; }

 

  • 六、JDK1.7和1.8的區別

一、JDK1.7的時候使用的是數組+ 單鏈表的數據結構。可是在JDK1.8及以後時,使用的是數組+鏈表+紅黑樹的數據結構(當鏈表的深度達到8的時候,也就是默認閾值,就會自動擴容把鏈表轉成紅黑樹的數據結構來把時間複雜度從O(n)變成O(logN)提升了效率)

二、JDK1.7用的是頭插法,而JDK1.8及以後使用的都是尾插法,那麼他們爲何要這樣作呢?由於JDK1.7是用單鏈表進行的縱向延伸,當採用頭插法時會容易出現逆序且環形鏈表死循環問題。可是在JDK1.8以後是由於加入了紅黑樹使用尾插法,可以避免出現逆序且鏈表死循環的問題。

三、擴容後數據存儲位置的計算方式也不同:1. 在JDK1.7的時候是直接用hash值和須要擴容的二進制數進行&(這裏就是爲何擴容的時候爲啥必定必須是2的多少次冪的緣由所在,由於若是隻有2的n次冪的狀況時最後一位二進制數才必定是1,這樣能最大程度減小hash碰撞)(hash值 & length-1),而在JDK1.8的時候直接用了JDK1.7的時候計算的規律,也就是擴容前的原始位置+擴容的大小值=JDK1.8的計算方式,而再也不是JDK1.7的那種異或的方法。可是這種方式就至關於只須要判斷Hash值的新增參與運算的位是0仍是1就直接迅速計算出了擴容後的儲存方式。

四、jdk1.7 先擴容再put ,jdk1.8 先put再擴容

 

參考:http://www.javashuo.com/article/p-eyugcawb-ch.html

相關文章
相關標籤/搜索