1. 程式人生 > 其它 >常見面試題:HashMap的底層原理?

常見面試題:HashMap的底層原理?

常見面試題:HashMap的底層原理?

在 JDK7 和 JDK8 中,HashMap 的底層是有所不同的

  • 在 JDK7 中,HashMap 是通過陣列+連結串列實現的
  • 在 JDK8 中,HashMap 是通過陣列+連結串列+紅黑樹實現的

我們通過原始碼來探討 JDK8 中 HashMap 的底層,主要是分析它的一些屬性,以及它的構造方法、put 方法和 get 方法

屬性

首先看一下 HashMap 裡都有哪些屬性,得有個大致瞭解:

//HashMap底層的那個陣列
transient Node<K,V>[] table;
//似乎和快取有關
transient Set<Map.Entry<K,V>> entrySet;
//記錄當前HashMap中有多少個Node
transient int size;
//記錄當前HashMap被修改了多少次,在出現執行緒不安全的問題時可以通過這個屬性來迅速報錯
transient int modCount;
//當size達到這個閾值後將對HashMap進行擴容,*****不過在table正式初始化前,用來記錄table的初始容量*****
int threshold;
//負載因子,在table正式初始化後,threshold就是通過capacity*loadFactor計算得到的
final float loadFactor;
//預設的table初始容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//table的最大容量
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;

構造方法

然後來看看 HashMap 的構造方法,雖然這裡呼叫的是無參的,但我們直接看最複雜的那個:

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);
}
  • 這兩個引數,一個是初始的陣列容量(無參時預設為16),另一個和擴容有關,叫做負載因子(無參時預設為0.75)

  • 這個建構函式其實也沒做什麼事情,就是對輸入的引數進行檢查,然後對 HashMap 的兩個屬性做初始化

  • 可以發現,在構造方法中其實並沒有對 table 進行初始化,此時 table 的初始容量舊記錄在 threshold

  • 需要注意的是,這裡 loadFactor 是直接賦值的,但 threshold 的賦值是經過處理的,看看 tableSizeFor 這個方法:

    /**
    * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
    
    • 直接看就懵逼了,因此我們可以帶入幾個值看看,比如說,如果我傳入的 capacity 是 10 的話,這個方法就會返回 16,如果傳入的 17 的話,這個方法就會返回 32,這也符合官方對這個方法的描述
    • 由此可見,HashMap 的陣列容量並不是任意的,一定得是 2 的冪,就算我們強行指定陣列容量為 10,也會經過這個方法的處理而變成 16
    • 此外,構造方法並沒有限制 initialCapacity 不能為 0,如果是 0 的話經過這個方法返回的還是 0,這其實算是個特殊情況吧,如果 initialCapacity 為 0,那麼在後續對 table 初始化時就會使用 DEFAULT_INITIAL_CAPACITY 作為其初始容量

put

接下來看一下 put 方法,是最重要的一個方法

首先需要明確,雖然我們是 put 了一個鍵值對,但其實最後存到 HashMap 裡的是一個 Node,這個 Node 有以下屬性:

static class Node<K,V> implements Map.Entry<K,V> {
	final int hash;
	final K key;
	V value;
	Node<K,V> next;
}

看看 put 的原始碼:

public V put(K key, V value) {
  return putVal(hash(key), key, value, false, true);
}
  • 可以發現 put 只是個馬甲,實際上是呼叫了 putVal,涉及五個引數,我們關注前三個,除了 key 和 value,還有根據 key 算出來的一個雜湊值,看看到底是怎麼算的:

    static final int hash(Object key) {
    	int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
  • 可以發現,這個雜湊值並不直接是物件 markword 裡的那個雜湊值,而是經過處理的

  • 如果 key 為 null,那麼雜湊值就是 0(說明 HashMap 是允許 null 作為 key 的)

  • 否則,先得到 markword 裡的雜湊值,記為 h,然後進行 h ^ (h >>> 16) 這個運算,運算結果才是 HashMap 真正使用的雜湊值

再來看看 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;
    
  //先對變數進行賦值,tab就是table的另一個引用,n就是table的容量
  //如果table還未初始化(或者容量為零),那麼進行初始化(或者擴容),並將新的容量賦值給n
  if ((tab = table) == null || (n = tab.length) == 0)
      //注意這裡的resize方法,初始化、擴容都是使用的這個方法
      n = (tab = resize()).length;
    
  //注意這裡的i = (n - 1) & hash,這裡其實是在計算這個node該放在table的哪個位置中,i就是最後算出的位置
  //這裡先將tab[i]處的內容交給了p
  //如果發現p為null,說明這個位置還什麼都沒有,此時直接新建一個node放進去就好
  if ((p = tab[i = (n - 1) & hash]) == null)
      tab[i] = newNode(hash, key, value, null);
    
  //如果p不為空,說明這個地方已經有東西了,這裡可能是個連結串列,也可能是個紅黑樹
  else {
      
      //定義一些變數
      Node<K,V> e;
      K k;
        
      //我們有時可能會put幾個相同key的node到HashMap中,此時新的value會覆蓋舊的value
      //這裡就是在判斷p的key和當前put的node的key是否相同
      //如果是,將p交給e,此時也不用管是連結串列還是紅黑樹,直接覆蓋就完事了
      if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
          //覆蓋操作會在後面統一處理
          e = p;
      
      //如果key不相同,那麼需要根據p是連結串列結點還是紅黑樹結點來分別處理
      //如果是紅黑樹結點,那麼呼叫putTreeVal方法將新的node插入到紅黑樹中
      else if (p instanceof TreeNode)
          //如果發現紅黑樹中存在相同key的node,會把這個node交給e,此時其實沒有插入,只需要後面進行覆蓋
          //否則返回null,表示成功插入了新的node
          e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
      
      //如果是連結串列結點
      else {
          //這裡是在遍歷連結串列,binCount記錄連結串列中有多少個結點
          for (int binCount = 0; ; ++binCount) {
              //如果發現遍歷到底了,說明連結串列中沒有相同key的node,那麼直接新建一個node插在最後
              if ((e = p.next) == null) {
                  p.next = newNode(hash, key, value, null);
                  //如果插入之後發現連結串列中結點個數超過了閾值(預設為大於8),將連結串列升級為紅黑樹
                  if (binCount >= TREEIFY_THRESHOLD - 1)
                      //注意,其實升級還需要另一個條件,隱藏在treeifyBin這個方法中,同時滿足才會升級
                      treeifyBin(tab, hash);
                  //都插入好了肯定可以break了
                  break;
              }
              //如果遍歷途中發現有相同key的node,那麼也可以break了,後面進行覆蓋即可
              if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                  break;
              p = e;
          }
      }
        
      //在上面的三種情況中,一旦發現有相同key的node,都會把這個node存放在e中,在這裡進行覆蓋
      if (e != null) {
          //得到舊的value
          V oldValue = e.value;
          if (!onlyIfAbsent || oldValue == null)
              //用新的value覆蓋舊的
              e.value = value;
          afterNodeAccess(e);
      }
  }
  //如果確實是插入了node,那麼modCount加一
  ++modCount;
  //size肯定也加一,如果加一後發現超過了閾值,那麼需要擴容
  if (++size > threshold)
      resize();
  afterNodeInsertion(evict);
  //如果確實是插入了node還不是覆蓋,put方法返回null
  return null;
}

可能會有些迷糊,這裡再捋一下:

  • 我們是在已經有了 hash key value 三樣東西之後進入 putVal 方法的

  • 首先,檢查 table 是否初始化了,如果初始化了,再檢查其容量是否為零,無論其中哪一種情況發生,顯然這個 HashMap 都是沒法使用的,因此需要對 table 進行初始化或者擴容,這兩個操作統一通過 resize 方法來完成,由於這個方法也比較複雜,因此放到最後再具體介紹

  • 現在,一個可用的 table 肯定已經有了,接下來就是根據 hash 來計算該把新的 node 放在 table 的哪個位置,把目標索引記為 i,那麼 i 的計算公式為 i = (n - 1) & hash,新的 node 就是放在 table[i] 這個地方

  • table[i] 這個地方可能是原本就有 node 的,不能直接就進行 table[i]=newNode 這種莽夫操作,因此,先把 table[i] 處的node 賦值給一個變數 p,接下來分四種情況進行討論:

    • 如果 p 為 null,說明這個地方沒有 node,那是最爽的,直接 table[i]=newNode 就完事了,新的 node 就插入成功了

    • 如果 p 不為 null,但 p 的 key 和當前要插入的 node 的 key 相同,說明此時要完成的不是插入,而是覆蓋,於是用新的 value 覆蓋舊的 value 就完事了

    • 如果 p 不為 null,p 的 key 和當前要插入的 node 的 key 也不同,此時就沒那麼方便了,因為我們不得不深入到連結串列或者紅黑樹中來進行插入的工作了

      • 如果發現 p 是一個紅黑樹結點,那麼就得在這個紅黑樹中進行插入操作,有關操作都被封裝在了 putTreeVal 這個方法中,我們也不必對這個方法過於糾結,因為比較複雜,而且和 HashMap 的關係並不大,我們只需要知道,如果發現樹中存在相同 key 的 node,那麼實際進行的也是覆蓋操作,而不是插入

      • 如果發現 p 是一個連結串列結點,那麼就得在這個連結串列中進行插入操作,這相比紅黑樹是比較簡單的,就是遍歷這個連結串列,如果途中發現某個相同 key 的 node,那麼進行覆蓋操作,但如果遍歷到底了也沒有發現相同 key 的 node,那就說明沒有,於是把新的 node 插入到連結串列的尾部即可,在插入完畢後,如果發現這個連結串列中結點的個數超過了閾值(預設為 8),就會呼叫 treeifyBin 這個方法,我們稍微看一下這個方法,可以發現,雖然方法的名字是樹化,但在方法中還會對 table 的容量做判斷,如果 table 的容量小於 MIN_TREEIFY_CAPACITY,那麼進行的其實是陣列擴容,而不是連結串列樹化:

        final void treeifyBin(Node<K,V>[] tab, int hash) {
            int n, index;
            Node<K,V> e;
            //注意這裡,如果table的容量小於MIN_TREEIFY_CAPACITY這個閾值(預設為64),其實也不會把連結串列升級為紅黑樹
            if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
                resize();
            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)
                        hd = p;
                    else {
                        p.prev = tl;
                        tl.next = p;
                    }
                    tl = p;
                } while ((e = e.next) != null);
                if ((tab[index] = hd) != null)
                    hd.treeify(tab);
            }
        }
        
  • 至此,如果上面完成的是覆蓋操作,那麼其實 putVal 方法已經完事了,但如果完成的是插入操作,最後還需要判斷 HashMap 中 node 的個數是否超過了閾值,如果超過了,需要進行一次擴容

現在,我們就剩下 resize 這個方法沒有看了,在看它之前,先梳理一下哪些情況下會呼叫它:

  • case1:嘗試插入 node 時發現 table 未初始化,或者 table 的容量為 0(我不是很清楚怎麼會有容量為 0 的情況發生,因此後面分析時為了簡潔起見就不管這種神奇的情況了)
  • case2:成功插入新 node 後,發現連結串列中結點個數大於 8,試圖進行連結串列樹化,但發現 table 的容量小於 64
  • case3:成功插入新 node 後,發現雜湊表中 node 的個數超過了 threshold

接下來就正式看一下 resize 這個方法:

final Node<K,V>[] resize() {
    
    //記錄舊的table(可能是未初始化的,因為初始化和擴容都是使用的這個方法)
    Node<K,V>[] oldTab = table;
    //記錄舊table的容量(如果未初始化就是0)
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //記錄舊table的閾值(如果未初始化,記錄的是要把table初始化為多大的容量)
    int oldThr = threshold;
    
    //新table的容量和閾值,後面一通操作就是給這兩個變數確定值
    int newCap, newThr = 0;
    
    //如果舊table是已經初始化的
    if (oldCap > 0) {
        //如果舊table的容量已經超過最大限制了,那麼已經不能再擴容了,只能放寬擴容的閾值,此時直接返回舊的table
        if (oldCap >= MAXIMUM_CAPACITY) {
        	threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //如果將舊容量翻倍後依舊沒有超過限制,並且舊容量是大於等於16的,將翻倍後的舊容量和舊閾值交給新容量和新閾值
        //****在我們的日常使用中絕大多數情況都是進的這裡,也就是說大多數情況下擴容都是把容量和閾值擴大為原來的兩倍****
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1;
    }
    
    //以下兩種都是table未初始化的情況,注意這裡的話oldThr的含義就不是舊閾值了,而是要把table初始化為多大的容量
    //當我們使用無參的建構函式,或者使用有參並且傳入了正常的initialCapacity,那麼oldThr肯定大於0並且一定是2的冪
    else if (oldThr > 0)
        //把oldThr交給newCap就完事了
        newCap = oldThr;
    //這就是前面提到過的特殊情況,當我們使用有參的建構函式但傳入了0作為initialCapacity,表明使用預設值
    else {               
        //使用預設的初始容量
        newCap = DEFAULT_INITIAL_CAPACITY;
        //使用預設的初始容量和預設的負載因子算出閾值
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    
    //新容量前面已經全部算好了(如果沒反應過來可以再體會一下),但如果新閾值還沒算出來,統一在這裡計算
    if (newThr == 0) {
        //先根據新容量和負載因子算出一個閾值ft
        float ft = (float)newCap * loadFactor;
        //如果新容量和ft都沒有超出限制,就使用ft作為新閾值,否則,使用INT_MAX作為新閾值
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    
    //將求出的新閾值交給threshold,此時閾值就真的更新好了,再下面就是根據新容量進行真正的擴容了
    threshold = newThr;
    
    //根據新容量開闢新的table
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    
    //如果舊table是未初始化的,裡面肯定沒東西,那麼resize就結束了,否則需要把舊table裡的東西挪到新table裡
    if (oldTab != null) {
        //遍歷舊table
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            //如果oldTab[j]處為空,那麼這個位置就不用管了,但如果oldTab[j]處不為空,先把這裡的node交給e
            if ((e = oldTab[j]) != null) {
                //oldTab[j]處肯定直接置空,不需要了
             	oldTab[j] = null;
                //如果只有一個node,那方便,直接把這唯一的一個node放到newTab裡
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                //但如果有多個node,就麻煩了
                //如果這是棵紅黑樹,那麼所有的轉移操作都封裝在split裡了,這裡就不深入了
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                //如果這是個連結串列
                else {
                    //構建兩個新連結串列lo和hi,分別有兩個變數指向首尾
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    //遍歷連結串列中的每個node
                    do {
                        next = e.next;
                        //如果雜湊值與舊容量相與結果為0,把node放到lo裡
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                 loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        //否則,把node放到hi裡
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    //收尾工作,把lo和hi放到newTab裡
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

雖然程式碼看起來有點複雜,但其實思路是比較直觀的,無論是為了初始化還是擴容,第一步都是算出接下來新 table 的容量和閾值,如果是為了初始化,那麼開闢好新的 table 就完事了,但如果是為了擴容,還得把舊 table 裡的內容轉移到新 table 裡

現在,我們對整個 put 的流程已經有所瞭解了,但裡面有一些地方其實是比較讓人疑惑的:

  • 在計算雜湊值時,為何不直接使用 markword 裡的雜湊值,而是要經過一些額外的運算?

    static final int hash(Object key) {
    	int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
    • 首先我們得知道,雜湊值很重要,涉及這個 node 最後會存放在 table 的哪個位置中
    • 如果直接使用 markword 裡的雜湊值,要知道我們是可以覆寫一個物件的 hashCode 方法的,如果我們寫的很爛,然後還在這裡直接用,可能會導致最後很多的 node 都放在 table 的同一個位置中,這對 HashMap 的效能影響是很大的
    • 因此,在這個方法裡會對 markword 裡的雜湊值做一些額外的運算(移位和異或),目的就是為了讓雜湊值變得更隨機,使得最後各個 node 可以儘量均勻的分佈在 table 中,提高 HashMap 的效能
  • 在計算要把 node 放在 table 的哪個位置中時,i = (n - 1) & hash 是什麼原理,為什麼不直接對 n 取餘?

    • 首先我們得知道,table 的容量是固定的,比如 16,那麼不管我們的雜湊值有多複雜,經過計算都得落在 0~15 這個範圍中

    • 直接用雜湊值對 16 取餘肯定是可行的,但取餘看著簡單,其實是比較耗時的,相比之下位運算的速度快很多

    • 那麼,i = (n - 1) & hash 是怎麼保證計算結果落在 0~15 這個範圍中的呢?

      • 首先,16 是 table 目前的容量,其二進位制為 10000,減一後為 15,二進位制為 1111

      • 我們的雜湊值是一個 32 位的整數,不管它有多大,高位有多少個 1,跟 1111 進行與操作後,相當於取了雜湊值的最後四位,因此結果一定是在 0000~1111 之間的,也就是 0~15,隨便看個例子:

        雜湊值	1011 1100 0010 0011 1111 1100 1001 1100 
        15    0000 0000 0000 0000 0000 0000 0000 1111 
        &     0000 0000 0000 0000 0000 0000 0000 1100
        
    • 既然通過位運算也能達到目標,並且速度還比取餘快,那自然就是採用這種方案了

  • 為什麼 table 的容量一定得是 2 的冪?

    • 直接舉個反例吧,如果 table 的容量為 17,那麼雜湊值經過計算一定得落在 0~16 這個範圍中:

      雜湊值	1011 1100 0010 0011 1111 1100 1001 1100 
      16    0000 0000 0000 0000 0000 0000 0001 0000
      &     0000 0000 0000 0000 0000 0000 0001 0000
      
    • 可以發現,在這種情況下,雖然運算結果也不會超出 0~16 的範圍,但其實只會出現 10000 和 00000 兩種結果,也就是說,無論我們的雜湊值是多少,最後這個 node 只會被放到 table[0] 或者 table[16] 的位置,這樣一來,剩下的位置統統都浪費了,所有 node 都扎堆在兩個位置裡,會導致 HashMap 的效能受到極大的影響

    • 因此,我們要求 table 的容量一定得是 2 的冪,因為 2 的冪減一之後,其二進位制肯定是 1111 這種形式,與雜湊值進行與操作後,首先肯定不會越界,其次保證了 table 中的每個位置都會被用上

  • 在擴容的轉移過程中,連結串列中的 node 是如何轉移的?lo 和 hi 這兩個連結串列是什麼意思?

    • 首先,我們想一下最暴力的轉移方式是什麼?其實很簡單,就是遍歷 oldTab[i] 處的每個 node,然後根據這個 node 的雜湊值和 table 的新容量來計算出這個 node 在 newTab 中的位置,即 hashCode & (newCapacity - 1),然後放進去即可

    • 但實際上,一個 node 在 newTab 中的位置只有兩種可能,假如原始容量為 16,新容量為 32,那麼 oldTab[i] 處的 node 只可能被轉移到 newTab[i] 和 newTab[i+16] 兩個位置

    • 我們舉個例子,假如原始容量為 16,新容量為 32,有 node1 和 node2 兩個 node:

      node1       	1011 1100 0010 0011 1111 1100 1001 1100
      oldCap-1		0000 0000 0000 0000 0000 0000 0000 1111
      oldPos      	0000 0000 0000 0000 0000 0000 0000 1100  -> 12
      
      node1       	1011 1100 0010 0011 1111 1100 1001 1100
      newCap-1		0000 0000 0000 0000 0000 0000 0001 1111
      newPos      	0000 0000 0000 0000 0000 0000 0001 1100  -> 12+16=28
      
      node2       	1011 1100 0010 0011 1111 1100 1000 1100
      oldCap-1		0000 0000 0000 0000 0000 0000 0000 1111
      oldPos       	0000 0000 0000 0000 0000 0000 0000 1100  -> 12
      
      node2       	1011 1100 0010 0011 1111 1100 1000 1100
      newCap-1		0000 0000 0000 0000 0000 0000 0001 1111
      newPos      	0000 0000 0000 0000 0000 0000 0000 1100  -> 12
      
    • 可以發現,由於容量總是 2 的冪,並且每次擴容都是翻倍,因此 newCap-1oldCap-1 其實只有一位 1 的差別(比如這裡只有第五位有區別),而 node 的雜湊值是不會改變的,因此,在和 newCap-1 進行與操作後,結果的後四位不可能發生改變,node 的新位置只取決於雜湊值的第五位是 0 還是 1

      • 如果是 0(比如 node2),那麼結果的第五位肯定還是 0,因此 node2 還是放在 i 的位置
      • 但如果是 1(比如 node1),那麼結果的第五位就變成 1 了,因此會放在 i+16 的位置
    • 因此,oldTab[i] 處的 node 只可能被轉移到 newTab[i] 和 newTab[i+16] 兩個位置,於是就產生了 lo 和 hi 這兩個連結串列,在轉移一個 node 時,先通過 hashCode & oldCap 來判斷雜湊值的第五位是 0 還是 1,如果是 0,就加入到 lo 中,如果是 1,就加入到 hi 中,所有 node 全部轉移完成後,再統一把 lo 放到 newTab[i] 的位置,把 hi 放到 newTab[i+16] 的位置,此時整個轉移的過程就結束了

get

在把 put 方法看明白後,get 就簡單多了:

public V get(Object key) {
	Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

可以發現 get 的主體是在 getNode 這個方法中,如果可以根據給定的 key,在 HashMap 中找到相同 key 的 node,那麼返回這個 node 的 value,否則返回 null

再來看看 getNode 這個方法:

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) {
        //如果這個位置上的node就是目標,直接返回
        if (first.hash == hash &&
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        //如果不止一個node,根據是紅黑樹還是連結串列分別討論
        if ((e = first.next) != null) {
            //如果是紅黑樹
            if (first instanceof TreeNode)
                //封裝好了,咱就不管了
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            //如果是連結串列
            do {
                //就是遍歷連結串列,逐個檢查是否有相同key的node,有的話就返回
                if (e.hash == hash &&
                   ((k = e.key) == key || (key != null && key.equals(k)))
                   return e;
            } while ((e = e.next) != null);
        }
    }
    //如果沒有找到相同key的node,只能返回null了                
    return null;
}

這個方法需要兩個引數,一個就是我們輸入的 key,還有一個是根據 key 計算出的雜湊值

首先進行一系列判斷,主要做了三件事情:

  • 保證 table 已經初始化了
  • 保證 table 的容量不為 0
  • 根據 (n - 1) & hash 算出目標 node 會在 table 的哪個位置中(雜湊值的作用),保證這個位置有 node 存在

如果上述三個條件有一個不滿足,那麼這個 HashMap 裡肯定沒有目標 node 了,直接返回 null 就完事了

否則,查詢過程如下:

  • 先看 table[i] 處的 node 是否就是目標 node,如果是,那最爽,直接返回這個 node 即可
  • 如果不是目標 node,看看這個 node 有沒有 next,也就是這個位置有沒有多個 node,如果這個位置就一個 node,那麼也挺爽,直接返回 null 即可
  • 如果這個位置有多個 node,就麻煩了,需要判斷這裡是連結串列還是紅黑樹
    • 如果是紅黑樹,具體查詢細節封裝在 getTreeNode 裡了,咱就不管了
    • 如果是連結串列,邏輯很簡單,就是遍歷一次連結串列,看有沒有目標 node,有的話就返回,沒有就返回 null