【JDK源碼分析系列】HashMap 源碼分析

【JDK源碼分析系列】HashMap 源碼分析

【0】HashMap 整體架構

【1】HashMap 新增數據流程

1. 空數組有無初始化,沒有的話初始化
2. 如果通過 key 的 hash 能夠直接找到值, 跳轉到 6,否則到 3
3. 如果 hash 衝突,兩種解決方案:鏈表 or 紅黑樹
4. 如果是鏈表,遞歸循環,把新元素追加到隊尾
5. 如果是紅黑樹,調用紅黑樹新增的方法
6. 通過 2、4、5 將新元素追加成功,再根據 onlyIfAbsent 判斷是否需要覆蓋
7. 判斷是否需要擴容,需要擴容進行擴容,結束

HashMap 代碼

public V put(K key, V value) {
    /**
     * 四個參數,
     * 第一個 : hash值
     * 第四個 : 表示如果該key存在值,且爲null的話,則插入新的value,
     * 第五個 : 在hashMap中沒有用,可以不用管,使用默認的即可
     */
    return putVal(hash(key), key, value, false, true);
}

//1:空數組初始化
//2:key計算的數組索引下,如果沒有值,直接新增賦值
//3:如果hash衝突,分成2種,一個是鏈表,一個是紅黑樹
//4:如果當前桶已經是紅黑樹了,調用紅黑樹新增的方法
//5:如果是鏈表,遞歸循環
//6:鏈表中的元素的key有和入參key相等的,允許覆蓋值的話直接覆蓋
//put方法默認覆蓋
//7:如果新增的元素在鏈表中不存在,則新增,新增到鏈表的尾部
//8:新增時,判斷如果鏈表的長度大於等於8時,轉紅黑樹
//9:如果數組的實際使用大小大於等於擴容的門檻,直接擴容
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                boolean evict) {
    // n 表示數組的長度, i 爲數組索引下標, p 爲 i 下標位置的 Node 值
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //如果數組爲空,初始化
    // 如果數組爲空, 使用 resize 方法初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // hashCode的算法先右移16 在並上數組大小-1
    // 如果當前索引位置是空的,直接生成新的節點在當前索引位置上
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 如果當前索引位置有值的處理方法, 即解決 hash 衝突
    else {
        // e 當前節點的臨時變量
        Node<K,V> e; K k;
        // 若節點的 hash key key 的值都相等則直接把當前下標位置的 Node 值賦值給臨時變量
        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) {
                //如果是最後一個,還找不到和新增的元素相等的,直接新增
                //節點是新增到鏈表最後的
                // e = p.next 表示從頭開始, 遍歷鏈表
                // p.next == null 表明 p 是鏈表的尾節點                    
                if ((e = p.next) == null) {
                    //p.next是新增的節點,但是e仍然是null
                    //e和p.next都是持有對null的引用,即使p.next後來賦予了值
                    // 只是改變了p.next指向的引用,和e沒有關係
                    // 把新節點放到鏈表的尾部
                    p.next = newNode(hash, key, value, null);
                    //新增時,鏈表的長度大於等於8時,鏈表轉紅黑樹
                    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 在遍歷過程中, 一直往後移動
                p = e;
            }
        }
        //說明新增的元素table中原來就有
        //說明新節點的新增位置已經找到了
        if (e != null) {
            V oldValue = e.value;
            // 當 onlyIfAbsent 爲 false 或者原始值爲 null 時,纔會覆蓋值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 當前節點移動到隊尾
            afterNodeAccess(e);
            // 返回老值
            return oldValue;
        }
    }
    // 記錄 HashMap 的數據結構發生了變化
    ++modCount;
    //如果kv的實際大小大於擴容的門檻,開始擴容
    //如果 HashMap 的實際大小大於擴容的門檻, 開始擴容
    if (++size > threshold)
        resize();
    // 刪除不經常使用的元素
    afterNodeInsertion(evict);
    return null;
}

【2】HashMap 查找數據流程

1. 根據hashcode,算出數組的索引,找到槽點
2. 槽點的key和查詢的key相等,直接返回
3. 槽點沒有next,返回null
4. 槽點有next,判斷是紅黑樹還是鏈表
5. 紅黑樹調用find,鏈表不斷循環

HashMap 代碼

public V get(Object key) {
    Node<K,V> e;
    //調用getNode方法來完成的
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

//1:根據hashcode,算出數組的索引,找到槽點
//2:槽點的key和查詢的key相等,直接返回
//3:槽點沒有next,返回null
//4:槽點有next,判斷是紅黑樹還是鏈表
//5:紅黑樹調用find,鏈表不斷循環
final Node<K,V> getNode(int hash, Object key) {
    // first 頭結點,e 臨時變量,n 長度,k key
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //數組不爲空 && hash算出來的索引下標有值
    //table不爲空 && table長度大於0 && table索引位置(根據hash值計算出)節點不爲空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //hash 和 key 的 hash 相等,直接返回
        // first的key等於傳入的key則返回first對象
        if (first.hash == hash &&
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        //hash不等,看看當前節點的 next 是否有值
        //first的key不等於傳入的key則說明是鏈表,向下遍歷
        if ((e = first.next) != null) {
            // 使用紅黑樹的查找
            // 判斷是否爲TreeNode,是則爲紅黑樹
            // 如果是紅黑樹節點,則調用紅黑樹的查找目標節點方法getTreeNode
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 採用自旋方式從鏈表中查找 key,e 爲鏈表的頭節點
            do {
                // 如果當前節點 hash == key 的 hash,並且 equals 相等,當前節點就是我們要找的節點
                //走下列步驟表示是鏈表,循環至節點的key與傳入的key值相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
                // 否則,把當前節點的下一個節點拿出來繼續尋找
            } while ((e = e.next) != null);
        }
    }
    //找不到符合的返回空
    return null;
}

【3】HashMap 刪除數據流程

HashMap 代碼

public V remove(Object key) {
    //臨時變量
    Node<K,V> e;
    /**
     * 調用removeNode(hash(key), key, null, false, true)進行刪除,
     * 第三個value爲null,表示,把key的節點直接都刪除了,不需要用到值,
     * 如果設爲值,則還需要去進行查找操作
     */
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

/**
 * 第一參數爲哈希值,
 * 第二個爲key,
 * 第三個value,
 * 第四個爲是爲true的話,則表示刪除它key對應的value,不刪除key,
 * 第四個如果爲false,則表示刪除後,不移動節點
 */
final Node<K,V> removeNode(int hash, Object key, Object value,
                            boolean matchValue, boolean movable) {
    // tab 哈希數組,p 數組下標的節點,n 長度,index 當前數組下標
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    // 哈希數組不爲null,且長度大於0,然後獲得到要刪除key的節點所在是數組下標位置
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        // nodee 存儲要刪除的節點,e 臨時變量,k 當前節點的key,v 當前節點的value
        Node<K,V> node = null, e; K k; V v;
        // 如果數組下標的節點正好是要刪除的節點,把值賦給臨時變量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)
                // 遍歷紅黑樹,找到該節點並返回
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else { // 表示爲鏈表節點,一樣的遍歷找到該節點
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                            (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    /**
                     * 注意,如果進入了鏈表中的遍歷,那麼此處的p不再是數組下標的節點,
                     * 而是要刪除結點的上一個結點
                     */
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        // 找到要刪除的節點後,判斷!matchValue
        if (node != null && (!matchValue || (v = node.value) == value ||
                                (value != null && value.equals(v)))) {
            // 如果刪除的節點是紅黑樹結構,則去紅黑樹中刪除
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            // 如果是鏈表結構,且刪除的節點爲數組下標節點,也就是頭結點,直接讓下一個作爲頭
            else if (node == p)
                tab[index] = node.next;
            else
                // 爲鏈表結構,刪除的節點在鏈表中,把要刪除的下一個結點設爲上一個結點的下一個節點
                p.next = node.next;
            // 修改計數器
            ++modCount;
            // 長度減一
            --size;
            // 此方法在hashMap中是爲了讓子類去實現,主要是對刪除結點後的鏈表關係進行處理
            afterNodeRemoval(node);
            // 返回刪除的節點
            return node;
        }
    }
    // 返回null則表示沒有該節點,刪除失敗
    return null;
}

致謝

本博客爲博主的學習實踐總結,並參考了衆多博主的博文,在此表示感謝,博主若有不足之處,請批評指正。

【1】【源碼解析】HashMap源碼跟進(紅黑樹的實現)

【2】30張圖帶你徹底理解紅黑樹

【3】【源碼解析】hashMap源碼跟進

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章