HashMap工作原理回顧 (基於JDK1.8源碼分析)

HashMap工作原理回顧 (基於JDK1.8源碼分析)

空的HashMap()構造方法

    /**
     * 默認初始容量爲16.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * 最大容量爲2的30次方,如果有參構造器指定了更大的數值,那麼仍然以2的30次方爲準
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 默認加載因子,用來在擴容的時候使用
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    /**
     * 空參構造器,使用初始容量16和默認加載因子0.75
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; 
    }

put(K key, V value) 方法

    /**
    * put方法源代碼
    */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
  • 我們可以看到方法內部實際調用了一個putVal方法,hash(key)是對key取hash值。
    /**
    *  putVal源代碼
    *  onlyIfAbsent參數:如果爲true,則不改變已存在的value。
    *  evict參數:該參數在HashMap中並沒有具體使用到,暫且忽略。
    */
    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爲空或長度爲0, 則初始化一個初始容量和默認閾值的Node數組
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

        // 經過(n-1)&hash這樣的邏輯與運算得到數組下標,在該數組下標位置的元素如果爲空,則根據參數構造一個Node放入該下標位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

        // 如果數組下標位置的Node不爲空    
        else {
            Node<K,V> e; K k;
            
            // 如果下標位置的節點的hash值與給定的hash值相同且key相同,那麼爲同一個Node,替換之。
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            
            // 如果Node爲樹節點的處理
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            
            // 如果下標位置的節點hash值以及key值對比不一樣(其中包括hash值相同而key值不同的情況,也就是我們常遇到的hash衝突,應如下處理)
            else {
                for (int binCount = 0; ; ++binCount) {

                    // 如果下標位置的節點next節點爲空,則指定該下標節點的next節點爲新節點,跳出循環
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }

                    // 如果下標位置的節點next節點不爲空且hash值及key相同,直接跳出循環。不然替換爲自身。
                    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;
        // 如果size超過閾值(如16*0.75=12),則調用resize方法進行擴容且以2的N次冪進行擴容。擴容過程中會對數組內節點的位置進行重新計算從而進行變動。
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

get(K key) 方法

    /**
    * 方法源碼
    */
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
  • 我們可以看到getNode方法,我們進入此方法看看其具體實現。
    /**
    * 此處的hash值與put方法中的hash值計算算法是一樣的,有興趣可以自己看下源碼
    */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;

        // 如果節點數組table不爲空且長度大於0,經過(n-1)&hash這樣的邏輯與運算得到下標後,在節點數組table中該下標位置的節點如果不爲空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            
            // 如果該下標位置的節點hash值與參數hash值相等且key相同,則返回該節點
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            
            // 如果該下標位置的節點hash值與參數hash值不相等或者key不相同,且該節點的next節點不爲空
            if ((e = first.next) != null) {

                // 處理如果是樹節點的情況
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                
                // 不斷循環查找next節點,直到找到匹配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;
    }

remove(K key) 方法

    /**
    * 方法源碼
    */
    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
  • 同樣的,我們可以看到一個removeNode方法,我們再看看這個方法的具體實現。
    /**
    * 此處的hash也是和put方法中的hash值運算算法相同得到的,
    * matchValue指是否需要匹配value值,movable指是否需要移除。
    */
    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;

        // 如果Node數組table不爲空且長度大於0,經過(n-1)&hash這樣邏輯與運算得到數組下標後,在數組該下標位置的Node不爲空
        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;

            // 該下標位置的節點hash值與指定hash值相等且key相同,則匹配
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            
            // 若下標位置的節點hash值與指定hash值不相等或key不相同
            else if ((e = p.next) != null) {
                
                // 處理樹節點的情況
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                
                // 循環查找next節點直到找到hash值以及key值都匹配的節點
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }

            // 如果匹配到hash值以及key值都相同的節點,且不需要匹配value或者value匹配也相等
            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);
                
                // 如果node節點與數組下標位置節點是同一個節點,那麼將下標位置指向node的next節點,這樣下標位置的原節點就不存在了。
                else if (node == p)
                    tab[index] = node.next;

                // 如果node節點與下標位置節點不是同一個節點,而是其鏈表中的其它節點(循環匹配到的其中一個next節點),那麼就循環後p節點(看循環代碼處實際是原來的e節點)的next節點指向node節點(看循環代碼處實際是新的e節點)的next節點。
                else
                    p.next = node.next;
                
                // 修改變動變量加一,容量減一。
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

至此,HashMap的三個常用的基本方法源碼分析就介紹到這裏,歡迎各位同僚提出批評與建議,轉載請說明出處,謝謝!

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