HashMap:內部組成&put:、get、remove方法大致邏輯&總結

源碼基於java1.8

一、傳統 HashMap的缺點
(1)JDK 1.8 以前 HashMap 的實現是 數組+鏈表,即使哈希函數取得再好,也很難達到元素百分百均勻分佈。
(2)當 HashMap 中有大量的元素都存放到同一個桶中時,這個桶下有一條長長的鏈表,這個時候 HashMap 就相當於一個單鏈表,假如單鏈表有 n 個元素,遍歷的時間複雜度就是 O(n),完全失去了它的優勢。
(3)針對這種情況,JDK 1.8 中引入了紅黑樹(查找時間複雜度爲 O(logn))來優化這個問題
引用自:https://blog.csdn.net/lianhuazy167/article/details/66967698

若桶中鏈表元素個數大於等於8時,鏈表轉換成樹結構;若桶中鏈表元素個數小於等於6時,樹結構還原成鏈表。
因爲紅黑樹的平均查找長度是log(n),長度爲8的時候,平均查找長度爲3,如果繼續使用鏈表,平均查找長度爲8/2=4,這纔有轉換爲樹的必要。鏈表長度如果是小於等於6,6/2=3,雖然速度也很快的,但是轉化爲樹結構和生成樹的時間並不會太短。
還有選擇6和8,中間有個差值7可以有效防止鏈表和樹頻繁轉換。假設一下,如果設計成鏈表個數超過8則鏈表轉換成樹結構,鏈表個數小於8則樹結構轉換成鏈表,如果一個HashMap不停的插入、刪除元素,鏈表個數在8左右徘徊,就會頻繁的發生樹轉鏈表、鏈表轉樹,效率會很低。
引用自:https://blog.csdn.net/xingfei_work/article/details/79637878

HashMap主要有以下幾個實例變量:

transient Node<K,V>[] table;
transient int size;
int threshold;
final float loadFactor;

PS:transient 表示易變的意思,在 Java 中,被該關鍵字修飾的變量不會被默認的序列化機制序列化。
table是一個Node類型的數組,稱爲哈希桶或哈希表,其中每個元素指向一個單向鏈表,鏈表中每一個節點表示一個鍵值對。Node是一個內部類,他的實例變量和構造方法如下:

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

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
}

1、hash是key的hash值,存儲hash值是爲了加快計算。
2、添加第一個元素時默認分配大小爲16,但並不是size大於16時再進行擴展,什麼時候擴展和threshold有關
3、threshold表示閾值,當size(鍵值對個數)大於等於threshold時考慮進行擴展。
4、loadFactor是負載因子,表示整體上table被佔用的成都,是一個浮點數,默認爲0.75,可以通過構造方法進行修改;
PS:一般來說threshold=table.length(默認16)*loadFactor

put方法大致邏輯

1.8的邏輯稍微複雜可參考上面的文章
該邏輯基於jdk1.7:

在這裏插入圖片描述
這裏借鑑網上的一張圖,對於1.8的邏輯描述:
在這裏插入圖片描述
1.8下HashMap的put方法:
PS:(length-1) & hash 能得到key在table中的位置;因爲length爲2的冪次方 (length-1) & hash等同於求模運算h%length.

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
  	/**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key // key 的哈希值
     * @param key the key 
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value // 如果爲true則不改變已經存在的值
     * @param evict if false, the table is in creation mode. // 不清楚幹啥的
     * @return previous value, or null if none // 舊值,key對應的值不存在時返回null
     */
    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爲map的Node數組
        // n爲數組的大小
        // i爲key對應的Node數組下標
        // p爲key對應的Node節點
        // PS:Node是一個單向鏈表
        if ((tab = table) == null || (n = tab.length) == 0) // 如果map爲空
            n = (tab = resize()).length; // resize()方法初始化或加倍,Node數組的大小
        if ((p = tab[i = (n - 1) & hash]) == null) // (n - 1) & hash 得到key對應的Node數組下標,並賦值給i;
            tab[i] = newNode(hash, key, value, null); // 如果key對應的Node爲null則創建常規(非樹)節點
        else {
            Node<K,V> e; K k;
            // k爲p的key
            // e表示key對應的Node對象
            // 下面這一堆if用來查找key對應的Node對象e
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))) // p的哈希值==key的hash值且[當前Node的key==key或(key不爲空且key   equals  當前節點的key)]
                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——存在key對應的Node對象
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e); // HashMap中該方法的實現爲空,作爲LinkedHashMap的回調,將節點移動到最後
                return oldValue;
            }
        }
        ++modCount; // 修改次數
        if (++size > threshold) // size大於等於threshold(閾值)時,調用resize進行擴容
            resize();
        afterNodeInsertion(evict); // HashMap中該方法的實現爲空,作爲LinkedHashMap的回調,用來判斷是否需要刪除最久未被訪問的節點,需要的話則刪除
        return null;
    }

被LinkedHashMap複寫的方法:

    // Create a regular (non-tree) node
    Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
        return new Node<>(hash, key, value, next);
    }

    // For conversion from TreeNodes to plain nodes
    Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
        return new Node<>(p.hash, p.key, p.value, next);
    }

    // Create a tree bin node
    TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
        return new TreeNode<>(hash, key, value, next);
    }

    // For treeifyBin
    TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
        return new TreeNode<>(p.hash, p.key, p.value, next);
    }

get方法(jdk1.8)

在這裏插入圖片描述
關於get和put的方法詳細介紹可以參考文首引用的文章,寫的相當不錯

remove方法(jdk1.8)

該部分轉自:https://blog.csdn.net/weixin_42340670/article/details/81139900

/**
* 從HashMap中刪除掉指定key對應的鍵值對,並返回被刪除的鍵值對的值
* 如果返回空,說明key可能不存在,也可能key對應的值就是null
* 如果想確定到底key是否存在可以使用containsKey方法
*/
public V remove(Object key) {
    Node<K,V> e; // 定義一個節點變量,用來存儲要被刪除的節點(鍵值對)
    return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value; // 調用removeNode方法
}
/**
* 方法爲final,不可被覆寫,子類可以通過實現afterNodeRemoval方法來增加自己的處理邏輯(解析中有描述)
*
* @param hash key的hash值,該值是通過hash(key)獲取到的
* @param key 要刪除的鍵值對的key
* @param value 要刪除的鍵值對的value,該值是否作爲刪除的條件取決於matchValue是否爲true
* @param matchValue 如果爲true,則當key對應的鍵值對的值equals(value)爲true時才刪除;否則不關心value的值
* @param movable 刪除後是否移動節點,如果爲false,則不移動
* @return 返回被刪除的節點對象,如果沒有刪除任何節點則返回null
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
                            boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index; // 聲明節點數組、當前節點、數組長度、索引值
    /*
     * 如果 節點數組tab不爲空、數組長度n大於0、根據hash定位到的節點對象p(該節點爲 樹的根節點 或 鏈表的首節點)不爲空
     * 需要從該節點p向下遍歷,找到那個和key匹配的節點對象
     */
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v; // 定義要返回的節點對象,聲明一個臨時節點變量、鍵變量、值變量
 
        // 如果當前節點的鍵和key相等,那麼當前節點就是要刪除的節點,賦值給node
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
 
        /*
         * 到這一步說明首節點沒有匹配上,那麼檢查下是否有next節點
         * 如果沒有next節點,就說明該節點所在位置上沒有發生hash碰撞, 就一個節點並且還沒匹配上,也就沒得刪了,最終也就返回null了
         * 如果存在next節點,就說明該數組位置上發生了hash碰撞,此時可能存在一個鏈表,也可能是一顆紅黑樹
         */
        else if ((e = p.next) != null) {
            // 如果當前節點是TreeNode類型,說明已經是一個紅黑樹,那麼調用getTreeNode方法從樹結構中查找滿足條件的節點
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            // 如果不是樹節點,那麼就是一個鏈表,只需要從頭到尾逐個節點比對即可    
            else {
                do {
                    // 如果e節點的鍵是否和key相等,e節點就是要刪除的節點,賦值給node變量,調出循環
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                            (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
 
                    // 走到這裏,說明e也沒有匹配上
                    p = e; // 把當前節點p指向e,這一步是讓p存儲的永遠下一次循環裏e的父節點,如果下一次e匹配上了,那麼p就是node的父節點
                } while ((e = e.next) != null); // 如果e存在下一個節點,那麼繼續去匹配下一個節點。直到匹配到某個節點跳出 或者 遍歷完鏈表所有節點
            }
        }
 
        /*
         * 如果node不爲空,說明根據key匹配到了要刪除的節點
         * 如果不需要對比value值  或者  需要對比value值但是value值也相等
         * 那麼就可以刪除該node節點了
         */
        if (node != null && (!matchValue || (v = node.value) == value ||
                                (value != null && value.equals(v)))) {
            if (node instanceof TreeNode) // 如果該節點是個TreeNode對象,說明此節點存在於紅黑樹結構中,調用removeTreeNode方法(該方法單獨解析)移除該節點
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p) // 如果該節點不是TreeNode對象,node == p 的意思是該node節點就是首節點
                tab[index] = node.next; // 由於刪除的是首節點,那麼直接將節點數組對應位置指向到第二個節點即可
            else // 如果node節點不是首節點,此時p是node的父節點,由於要刪除node,所有隻需要把p的下一個節點指向到node的下一個節點即可把node從鏈表中刪除了
                p.next = node.next;
            ++modCount; // HashMap的修改次數遞增
            --size; // HashMap的元素個數遞減
            afterNodeRemoval(node); // 調用afterNodeRemoval方法,該方法HashMap沒有任何實現邏輯,目的是爲了讓子類根據需要自行覆寫
            return node;
        }
    }
    return null;
}

PS:(length-1) & hash 能得到key在table中的位置;因爲length爲2的冪次方 (length-1) & hash等同於求模運算h%length.

小結:

HashMap存取時都根據鍵的hash值,只在對應的鏈表中操作,不訪問別的鏈表,在對應的鏈表進行操作時也是線比較hash值,如果hash值相同再用equals進行比較。這就要求,相同對象的hashCode返回值必須相同;
1、因爲內部使用鏈表,紅黑樹,和哈希值的方式實現,所以保存和取值的效率都很高,爲O(1),每個單向鏈表往往只有一個或少數節點,根據hash值就可以快速定位;
2、HashMap只能工的鍵值對沒有順序,因爲hash值是隨機的
如果經常存取,且不要求順序,那麼HashMap是理想的選擇。如果要求保持添加順序,可以使用LinkedHashMap或TreeMap
3、 HashMap不是線程安全的,Java中還有一個Hashtable,他是java早起的容器類之一,實現了map接口,原理和HashMap類似,但沒有特別的優化,它內部同事synchronized實現了線程安全。但是高併發場場景中推薦使用ConcurrentHashMap。

好了寫的很多了,大家有什麼指導或疑問歡迎評論溝通

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