Java中HashMap的put與get方法原理


直接上代碼

注: 代碼來自於 Java 9

put方法

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
  • 當調用put(),首先會根據key生成一個 hash值,原理如下:
static final int hash(Object key) {
    int h;
    //key 是 null 直接返回 0
    //key 不是null,先計算key對應的hashCode,賦值給 h
    //並將 h 與 h >>> 16 做異或(相同為0 不同為1)運算 ,作為hash值返回
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • 下圖舉例說明了位運算的過程,至於原理解釋,參考本文引用

  • 拿到了hash值后,調用 putVal(),做了如下操作

    • 將對象table賦值給tab,並以tab是否為空作為是否第一次調用此方法的判斷,是則resize()並給tab,n賦值;
    • 獲取tab的第i個元素:根據 (n - 1) & hash 算法 ,計算出i找到,如果為空,調用newNode() ,賦值給tab第i個;
    • 如果不為空,可能存在2種情況:hash值重復了,也就是put過程中,發現之前已經有了此key對應的value,則暫時e = p;
      至於另外一種情況就是位置沖突了,即根據(n - 1) & hash算法發生了碰撞,再次分情況討論;
      1.以鏈表的形式存入;
      2.如果碰撞導致鏈表過長(大於等於TREEIFY_THRESHOLD),就把鏈表轉換成紅黑樹;
    • 最后,如果e不為空,將e添加到table中(e.value 被賦值為 putVal()中的參數 value);

代碼如下:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //hashmap對象中 tabel屬性為空--->第一次put---->resize()
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //發現tab[i] 沒有值,直接存入即可
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        //tab[i]取到值了,莫慌,先定義下方2個變量
        Node<K,V> e; K k;
        //如果是 key 重復了  很簡單,直接e = p
        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) {
                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;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

get方法

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
  • 還是先根據key獲取hash值,其他沒什么可說的,有值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) {
        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;
}
  • 不難發現,此方法明顯沒有putVal復雜,並且參數比較簡單:一個int型的hash值,一個Object的key;
  • 先定義幾個變量:
    • 1個Node的數組 tab,兩個Node對象,first,e,一個int n,一個K k;
  • 進入方法的if判斷,如果不走此if,直接返回null;
    • 判斷了如下內容,並且用 && 連接(同時滿足,並且有短路)
    • (tab = table) != null, 只要進行過 put 操作,即滿足;
    • (n = tab.length) > 0,要求map集合中有元素(與上一個條件不同:先put再remove,此判斷不成立);
    • (first = tab[(n - 1) & hash]) != null,還是與put時同樣的計算索引方法,!=null 代表tab數組對應索引有元素;
  • 滿足最外層的if后,再次需要分2種情況討論;
    • 別找了 hash值也是first的hash值,傳入的key也是那個key(==直接返回true,重寫了 equal后 返回true也可以)
      此時,直接返回first即可;
    • 樹中還是鏈表中?做出不同處理
      1.紅黑樹:直接調用getTreeNode(),不做深究
      2.鏈表:通過.next() 循環獲取,知道找到滿足條件的key為止
  • 最后,可以返回之前定義的 Node對象 e啦。

再來兩張圖,加深理解

  • 從結構實現來講,HashMap是數組+鏈表+紅黑樹(JDK1.8增加了紅黑樹部分)實現的,如下如所示。

  • HashMap的put方法執行過程可以通過下圖來理解。

本文參考:http://yikun.github.io/2015/04/01/Java-HashMap工作原理及實現/
http://www.importnew.com/20386.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM