深刻理解HashMap(五): 關鍵源碼逐行分析之put

前言

系列文章目錄java

上一篇咱們討論了HashMap的擴容操做, 提到擴容操做發生在table的初始化或者table大小超過threshold後,而這兩個條件的觸發基本上就發生在put操做中。segmentfault

本篇咱們就來聊聊HashMap的put操做。數組

本文的源碼基於 jdk8 版本.app

put方法

HashMap 實現了Map接口, 所以必需要實現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) */
}

能夠看到, put方法是有返回值的, 這裏調用了 putVal 方法, 這個方法很重要, 咱們將經過代碼註釋的方式逐行說明.性能

在這以前咱們先看該方法的參數:this

  • hash

由上面的調用可知, 該值爲hash(key), 是key的hash值, 關於hash的概念以前已經講過了, 這裏再也不贅述.code

  • key, value

待存儲的鍵值對接口

  • onlyIfAbsent

這個參數用於決定待存儲的key已經存在的狀況下,要不要用新值覆蓋原有的value, 若是爲true, 則保留原有值, false 則覆蓋原有值, 從上面的調用看, 該值爲false, 說明當key值已經存在時, 會直接覆蓋原有值。get

  • evict

該參數用來區分當前是不是構造模式, 咱們在講解構造函數的時候曾經提到,HashMap的第四個構造函數能夠經過已經存在的Map初始化一個HashMap, 若是爲 false, 說明在構造模式下, 這裏咱們是用在put函數而不是構造函數裏面, 因此爲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是不是空的
    // 咱們知道, HashMap的三個構造函數中, 都不會初始Table, 所以第一次put值時, table必定是空的, 須要初始化
    // table的初始化用到了resize函數, 這個咱們上一篇文章已經講過了
    // 因而可知table的初始化是延遲到put操做中的
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
        
    // 這裏利用 `(n-1) & hash` 方法計算 key 所對應的下標
    // 若是key所對應的桶裏面沒有值, 咱們就新建一個Node放入桶裏面
    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值相等必須知足兩個條件
        //    1. hash值相同
        //    2. 二者 `==` 或者 `equals` 等
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p; // key已經存在的狀況下, e保存原有的鍵值對
        
        // 到這裏說明要保存的桶已經被佔用, 且被佔用的位置存放的key與待存儲的key值不一致
        
        // 前面已經說過, 當鏈表長度超過8時, 會用紅黑樹存儲, 這裏就是判斷存儲桶中放的是鏈表仍是紅黑樹
        else if (p instanceof TreeNode)
            // 紅黑樹的部分之後有機會再說吧
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        
        //到這裏說明是鏈表存儲, 咱們須要順序遍歷鏈表
        else {
            for (int binCount = 0; ; ++binCount) {
                // 若是已經找到了鏈表的尾節點了,尚未找到目標key, 則說明目標key不存在,那咱們就新建一個節點, 把它接在尾節點的後面
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 若是鏈表的長度達到了8個, 就將鏈表轉換成紅黑數以提高查找性能
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 若是在鏈表中找到了目標key則直接退出
                // 退出時e保存的是目標key的鍵值對
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        
        // 到這裏說明要麼待存儲的key存在, e保存已經存在的值
        // 要麼待存儲的key不存在, 則已經新建了Node將key值插入, e的值爲Null
        
        // 若是待存儲的key值已經存在
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            
            // 前面已經解釋過, onlyIfAbsent的意思
            // 這裏是說舊值存在或者舊值爲null的狀況下, 用新值覆蓋舊值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e); //這個函數只在LinkedHashMap中用到, 這裏是空函數
            // 返回舊值
            return oldValue;
        }
    }
    
    // 到這裏說明table中不存在待存儲的key, 而且咱們已經將新的key插入進數組了
    
    ++modCount; // 這個暫時用不到
    
    // 由於又插入了新值, 因此咱們得把數組大小加1, 並判斷是否須要從新擴容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict); //這個函數只在LinkedHashMap中用到, 這裏是空函數
    return null;
}

總結

  1. 在put以前會檢查table是否爲空,說明table真正的初始化並非發生在構造函數中, 而是發生在第一次put的時候。
  2. 查找當前key是否存在的條件是p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))
  3. 若是插入的key值不存在,則值會插入到鏈表的末尾。
  4. 每次插入操做結束後,都會檢查當前table節點數是否大於threshold, 若超過,則擴容。
  5. 當鏈表長度超過TREEIFY_THRESHOLD(默認是8)個時,會將鏈表轉換成紅黑樹以提高查找性能。

(完)

查看更多系列文章:系列文章目錄

相關文章
相關標籤/搜索