細探HashMap(Java1.8)

餘生很長,學精學透。

常量

源碼中定義的一些常量和方法都表示爲靜態變量,如下:

    // 默認容積
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    // 最大容積
    static final int MAXIMUM_CAPACITY = 1 << 30;
    // 當存量達到容積的0.75倍時,擴容
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 當桶中bin的數量超過該閾值時,不再用鏈表存儲,改用樹存儲(紅黑樹)
    static final int TREEIFY_THRESHOLD = 8;
    // 當桶中bin數量小於該值時,不再用樹存儲,改用鏈表存儲
    static final int UNTREEIFY_THRESHOLD = 6;
    // 被樹化時的最小容量
    static final int MIN_TREEIFY_CAPACITY = 64;

計算哈希值的方法:

static final int hash(Object key){};

當然,還有一些信息是不希望被序列化的,用transient修飾了:

    // 存儲node節點的表
    transient Node<K,V>[] table;

    /**
     * Holds cached entrySet(). Note that AbstractMap fields are used
     * for keySet() and values().
     */
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * The number of key-value mappings contained in this map.
     */
    transient int size;

    /**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     */
    transient int modCount;

主要常量這些,還有一些默認修飾的,如threshold等;

構造函數

一共有4種。

/*
* 傳入容量和裝載因子
*/
public HashMap(int initialCapacity, float loadFactor) {}
/*
* 傳入容量,裝載因子用默認的
*/
public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
/*
* 容量和裝載因子都使用默認的
*/
public HashMap(){}
/*
* 參數使用默認的,然後把注入的map一個個傳給本HashMap
*/
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

Node類

默認情況下,存在HashMap中的鍵值對都是以鏈表的形式存放在node類中。代碼也並不複雜,如下:

    /**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     */
    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;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

裏面就存放 了hash值、key、value和nextNode。

TreeNode類

實現比較複雜,繼承了LinkedHashMap.Entry<K,V>,而後者是繼承的HashMap.Node<K,V>,皮球繞了一圈又回到了Map.Entry<K,V>。紅黑樹比較複雜,先了解紅黑樹再看源碼,紅黑樹的五大性質:

1)每個結點要麼是紅的,要麼是黑的。
2)根結點是黑的。
3)每個葉結點,即空結點(NIL)是黑的。
4)如果一個結點是紅的,那麼它的倆個兒子都是黑的。
5)對每個結點,從該結點到其子孫結點的所有路徑上包含相同數目的黑結點。

紅黑樹比較複雜,還沒看懂……

在何種情況下,單向鏈表會轉換成treenode?

在HashMap最開始的註釋中,轉換的情況是這樣描述的:

     * Because TreeNodes are about twice the size of regular nodes, we
     * use them only when bins contain enough nodes to warrant use
     * (see TREEIFY_THRESHOLD). And when they become too small (due to
     * removal or resizing) they are converted back to plain bins.  In
     * usages with well-distributed user hashCodes, tree bins are
     * rarely used.  Ideally, under random hashCodes, the frequency of
     * nodes in bins follows a Poisson distribution
     * (http://en.wikipedia.org/wiki/Poisson_distribution) with a
     * parameter of about 0.5 on average for the default resizing
     * threshold of 0.75, although with a large variance because of
     * resizing granularity. Ignoring variance, the expected
     * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
     * factorial(k)).

我試着翻譯一下:

鑑於TreeNodes一般都是有規則節點(鏈表節點?)的兩倍長度,所以我們只有在節點足夠多的時候才使用TreeNode。當節點數減少到足夠小的時候,TreeNodes就會轉換爲普通節點(單向鏈表)。在使用的哈希值分佈比較好的時候,很少使用樹。理想情況下,在閾值爲0.75時節點在箱中的分佈遵循一個平均參數爲0.5的泊松分佈,雖然調整粒度但方差還是挺大的。忽略方差,期望分佈滿足這個公式。

好吧!這裏只是說節點多的情況下會轉換爲樹,還是沒有寫清楚具體啥時候轉。既然前面已經說了,最開始是regular node,那說明插入數據達到一定程度後,就將錶轉爲樹,看put代碼順藤摸瓜,在putVal方法中有提現:

            Node<K,V> e; K k;
            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是判斷是否哈希值、key都相同,就是判斷是不是插入同樣的數據;第二else if 判斷是否已經是樹了;第三個是正常插入操作,看代碼可以看出來,是一個無限循環,直到找到節點p的next節點是空的,或者p的next節點就是這個插入的數據,如果p一直往next節點找,找到空並且箱數超過限定的轉樹閾值(默認是8),那就執行treeifyBin操作,即把table轉換爲紅黑樹。

所以,重點看看這個方法。

    /**
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

從這個方法我們就可以看出,桶數組中,如果對應的某個桶的單向鏈路中bincount大於7,且桶的capacity大於64(均默認情況下),那麼此鏈路就會轉換爲紅黑樹,而桶數組的其他單向鏈路則仍然是單向鏈路,不需要桶化。

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