HashMap底層原理解析

原文地址: https://jygod.github.io/2018/04/05/HashMap%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E8%A7%A3%E6%9E%90/

初始化

我們先來看看在初始化HashMap的時(shí)候會(huì)發(fā)生神馬:

HashMap<String, Person> map = new HashMap<String, Person>();

 public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

只會(huì)初始化一個(gè)參數(shù)loadFactor(負(fù)載因子),DEFAULT_LOAD_FACTOR 是負(fù)載因子的默認(rèn)值0.75f。

 /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

那么我們可以知道初始化HashMap基本上什么都沒干...

然后我們進(jìn)行下一個(gè)語句 Person p = new Person("Jiang", 18);map.put("jy", p);

這是put()方法源碼:

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

然后是putVal()方法:

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else
            ...
            ... // 先過濾掉,反正剛開始不會(huì)往這里面走...
            ...
          ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
 }

我們先來看看這里的table是什么?

 /**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
    transient Node<K,V>[] table;

相當(dāng)于是一個(gè)初始值為null的Node數(shù)組,再來看看Node的數(shù)據(jù)結(jié)構(gòu):

 /**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
    }

回到putVal()方法中...我們首次put元素的時(shí)候,這個(gè)table當(dāng)然是null的,因此會(huì)執(zhí)行 table = resize();

resize()方法就是HashMap進(jìn)行動(dòng)態(tài)擴(kuò)容的關(guān)鍵方法。

由于resize()方法有點(diǎn)長(zhǎng)..我僅把首次擴(kuò)容的關(guān)鍵代碼截下來...

 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) {
            ...// 不會(huì)走這里
        }
        else {  // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY; // static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        threshold = newThr;
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            ..... // 不會(huì)走這里
        }
      return newTab;
 }

我們很清楚的知道,由于 table 是null,所以 oldCap = 0;所以有:

newCap = 1 << 4 = 16; newThr = 0.75f * 16 = 12;

Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;

這兩行表示 table 現(xiàn)在就是正式在內(nèi)存堆里面開辟的大小為16的Node數(shù)組了~

現(xiàn)在的狀態(tài)差不多是介個(gè)樣子滴:

img1.jpeg

我們的元素此時(shí)還沒有加到map中,因?yàn)槲覀?code>putVal()方法還沒有分析完,只是對(duì)底層的table進(jìn)行了初始化的擴(kuò)容,接下來我們繼續(xù)分析putVal()方法,看看具體怎么把元素塞進(jìn)map中的。

為了防止往上翻代碼....

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0) // 剛才我們分析道這兒了?。?!
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null) // 現(xiàn)在從這兒開始...
            tab[i] = newNode(hash, key, value, null);
        else
            ...
            ... // 先過濾掉,反正剛開始不會(huì)往這里面走...
            ...
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
 }

resize()擴(kuò)容之后,通過 (n - 1) & hash 計(jì)算得到一個(gè) 0~15的下標(biāo)值,得到的下標(biāo) i 正式我們?cè)貞?yīng)該放入的table中的位置,如果計(jì)算后的i在table中的位置 table[i] 為null, 證明當(dāng)前位置還沒有任何節(jié)點(diǎn)元素,我們需要new一個(gè)節(jié)點(diǎn),并放在table[i]上。

所以現(xiàn)在的狀態(tài)應(yīng)該是這樣了QAQ:

img2.jpeg

而每一次put()元素的時(shí)候,如果在table[i]上元素為null, 將元素插入table[i]時(shí),都會(huì)進(jìn)行size++ 的操作, size對(duì)應(yīng)上圖HashMap對(duì)象的那個(gè)size:

 if (++size > threshold)
            resize();

因此,雖說在初始化時(shí)底層建立了一個(gè)16長(zhǎng)度的數(shù)組,但是hashMap得邏輯長(zhǎng)度最開始仍然是0, 只有在每次table[i]中有新元素到來的時(shí)候(并且table[i]為null)才會(huì)增加size;

而當(dāng)size也就是添加的元素個(gè)數(shù)超過閾值threshold,就會(huì)進(jìn)行resize()底層數(shù)組的擴(kuò)容~ 這個(gè)我們之后再詳細(xì)說明。

HashMap的初始化操作就到這里了~

添加元素以及解決沖突

我們之前分析了向底層table添加新元素的過程,現(xiàn)在我們執(zhí)行Person p1 = new Person("jiang", 20);map.put("jyjy", p1);來模擬Hash沖突的情況。

還是先來分析putVal()方法:

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
     Node<K,V>[] tab; Node<K,V> p; int n, i;
     .... // 省略
     ....
       if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else { // 這個(gè)時(shí)候應(yīng)該是走這里啦
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))) // 如果發(fā)現(xiàn)要插入的元素的vale存在,則啥也不干..
                e = p;
            else if (p instanceof TreeNode) // 如果插入時(shí)發(fā)現(xiàn)是TreeNode結(jié)構(gòu)的
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else { // 轉(zhuǎn)換成紅黑樹前的插入操作
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
     ....
     ....
 }

可以看到注釋里寫的,再插入時(shí),有兩種可能的結(jié)構(gòu)。JDK1.7及以前版本中只會(huì)存在單鏈表的結(jié)構(gòu),而這種單鏈表的結(jié)構(gòu),一旦元素太多了,就會(huì)導(dǎo)致查詢效率低下... 因此在JDK1.8的版本中,如果元素超過插入的閾值(8個(gè)),就會(huì)將單鏈表轉(zhuǎn)換成紅黑二叉樹的結(jié)構(gòu),這里我們知道就好,具體的紅黑二叉樹轉(zhuǎn)換和插入比較復(fù)雜,就不在這篇文章中分析了...

那么我們需要關(guān)注的實(shí)際就是這一段代碼:

      else { // 轉(zhuǎn)換成紅黑樹前的插入操作
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) { //在最后一個(gè)位置new一個(gè)新節(jié)點(diǎn)
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // 超過閾值轉(zhuǎn)換成紅黑樹
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }

這段代碼很好理解,從第一個(gè)節(jié)點(diǎn)開始向后遍歷,在鏈表的末尾插入新的節(jié)點(diǎn),如果超過閾值(8個(gè)),則轉(zhuǎn)換成紅黑二叉樹。

因此現(xiàn)在的狀態(tài)理論上是醬紫:

img3.jpeg

再來看最后執(zhí)行的這段:

  if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }

插入節(jié)點(diǎn)或是發(fā)現(xiàn)已有一樣的節(jié)點(diǎn)后,e都不會(huì)為null(只有一開始table[i]為null的時(shí)候e才會(huì)是null),所以上一段必定會(huì)執(zhí)行。

因此會(huì)直接return這個(gè)插入節(jié)點(diǎn)的value,而不會(huì)執(zhí)行之后的size++;

擴(kuò)容

在初始化的時(shí)候,我們看到putVal()這個(gè)方法里最后有這么一句話:

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
     ....
     ....// 前面省略
         
      if (++size > threshold)
            resize();
 }

初始的時(shí)候 threshold 的值我們計(jì)算得到的是 0.75f * 16 = 12,因此如果插入到table的元素超過閾值時(shí)(現(xiàn)在閾值為12)會(huì)觸發(fā)resize()對(duì)當(dāng)前table數(shù)組擴(kuò)容。

又來到了resize()方法,只不過這一次就是真的要對(duì)底層數(shù)組進(jìn)行擴(kuò)容了~

 final Node<K,V>[] resize() {
      .....
      if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
 }

現(xiàn)在oldCap是16 > 0,因此往下走,newCap = 16 << 1 = 16 * 2 = 32,擴(kuò)大兩倍。而閾值也是擴(kuò)大兩倍。

繼續(xù)往下面看...

 final Node<K,V>[] resize() {
      .....
      if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
     ...
         
     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;
                        }
                    }
                }
            }
        }
 }

是不是感覺有點(diǎn)長(zhǎng)啊... 沒事,慢慢來… ..這一段是在干什么呢?在底層table數(shù)組擴(kuò)容一倍之后,需要重新計(jì)算每個(gè)元素的放置位置,在JDK1.7及以前,其實(shí)都是根據(jù)rehash()重新計(jì)算節(jié)點(diǎn)的hash值然后再 e.hash & (oldCap - 1) 來重新計(jì)算節(jié)點(diǎn)的下標(biāo)位置,進(jìn)而進(jìn)行重新的節(jié)點(diǎn)排列。

而JDK1.8優(yōu)化之后不需要再次對(duì)元素的hash值進(jìn)行計(jì)算,而是只會(huì)將之前元素的hash值和oldCap值進(jìn)行對(duì)比,觀察其最高位的0,1情況,具體如下:

(e.hash & oldCap) 得到的是 元素的在數(shù)組中的位置是否需要移動(dòng),示例如下

示例1:
e.hash=10 0000 1010
oldCap=16 0001 0000
&   =0  0000 0000       比較高位的第一位 0
結(jié)論:元素位置在擴(kuò)容后數(shù)組中的位置沒有發(fā)生改變

示例2:
e.hash=17 0001 0001
oldCap=16 0001 0000
&   =1  0001 0000      比較高位的第一位   1
結(jié)論:元素位置在擴(kuò)容后數(shù)組中的位置發(fā)生了改變,新的下標(biāo)位置是原下標(biāo)位置+原數(shù)組長(zhǎng)度

將結(jié)果為0的存在以loHead為首的鏈表中, 將結(jié)果為1的存在以hiHead為首的鏈表中,然后分別放入table[i]和table[1 + oldCap]中。

這樣做能夠有效地將元素分散。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容