JDK原始碼閱讀(4):HashMap類閱讀筆記

pedro7 發表於 2021-10-10
HashMap

HashMap

public class HashMap<K, V> extends AbstractMap<K, V>
        implements Map<K, V>, Cloneable, Serializable {
}

1. 一些重要引數

1.1 serialVersionUID屬性

// 序列化版本號
private static final long serialVersionUID = 362498820763181265L;

serialVersionUID適用於java序列化機制。簡單來說,JAVA序列化的機制是通過 判斷類的serialVersionUID來驗證的版本一致的。在進行反序列化時,JVM會把傳來的位元組流中的serialVersionUID於本地相應實體類的serialVersionUID進行比較。如果相同說明是一致的,可以進行反序列化,否則會出現反序列化版本一致的異常,即是InvalidCastException。

具體序列化的過程是這樣的:序列化操作時會把系統當前類的serialVersionUID寫入到序列化檔案中。當反序列化時系統會自動檢測檔案中的serialVersionUID,判斷它是否與當前類中的serialVersionUID一致。如果一致說明序列化檔案的版本與當前類的版本是一樣的,可以反序列化成功,否則就失敗;

1.2 DEFAULT_INITIAL_CAPACITY屬性

/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

DEFAULT_INITIAL_CAPACITYHashMap的預設初始容量,大小為16

1.3 DEFAULT_LOAD_FACTOR屬性

/**
 * The load factor used when none specified in constructor.
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

DEFAULT_LOAD_FACTORHashMap的預設負載因子大小,大小為0.75。

元素數量 ≥ 容量*負載因子​,那麼HashMap需要擴容。

1.4 MAXIMUM_CAPACITY屬性

/**
 * The maximum capacity, used if a higher value is implicitly specified  
 * by either of the constructors with arguments.
 * MUST be a power of two <= 1<<30.
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

MAXIMUM_CAPACITY屬性是HashMap的最大容量。

1.5 TREEIFY_THRESHOLD屬性

/**
 * The bin count threshold for using a tree rather than list for a
 * bin.  Bins are converted to trees when adding an element to a
 * bin with at least this many nodes. The value must be greater
 * than 2 and should be at least 8 to mesh with assumptions in
 * tree removal about conversion back to plain bins upon
 * shrinkage.
 */
static final int TREEIFY_THRESHOLD = 8;

TREEIFY_THRESHOLD屬性是HashMap轉變為紅黑樹實現的閾值。當元素數量大於這個值,就會轉變為紅黑樹。

1.6 UNTREEIFY_THRESHOLD屬性

UNTREEIFY_THRESHOLDHashMap由紅黑樹轉變為連結串列的閾值。當元素數量大於這個值,就會轉變為連結串列。

1.7 MIN_TREEIFY_CAPACITY屬性

MIN_TREEIFY_CAPACITY是可以將連結串列轉變為紅黑樹的最小陣列容量。如果沒有這個限制,假如節點過多,節點陣列會頻繁地resize

2. 一些重要屬性

2.1 table屬性

/**
 * The table, initialized on first use, and resized as
 * necessary. When allocated, length is always a power of two.
 * (We also tolerate length zero in some operations to allow
 * bootstrapping mechanics that are currently not needed.)
 */
transient Node<K, V>[] table;

table屬性用來存節點。

2.2 entrySet屬性

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

entrySet屬性用來快取鍵值對集合。

2.3 size屬性

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

size屬性用來存map的鍵值對數目。

2.4 modCount屬性

/**
 * 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;

modCount屬性記錄了map結構被修改的次數,用於在迭代器中的快速失敗。

2.5 threshold屬性

/**
 * The next size value at which to resize (capacity * load factor).
 *
 * @serial
 */
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
int threshold;

threshold屬性記錄了下一個需要resize的size大小

2.6 loadFactor屬性

/**
 * The load factor for the hash table.
 *
 * @serial
 */
final float loadFactor;

loadFactor屬性記錄了當前map的負載因子

2.7 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;
    }

    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;
    }
}

Node內部類實則表達了一個鍵值對, 幷包含了雜湊值和next指標。

2.8 EntrySet內部類

final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
    public final int size()                 { return size; }
    public final void clear()               { HashMap.this.clear(); }
    public final Iterator<Map.Entry<K,V>> iterator() {
        return new EntryIterator();
    }
    public final boolean contains(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>) o;
        Object key = e.getKey();
        Node<K,V> candidate = getNode(hash(key), key);
        return candidate != null && candidate.equals(e);
    }
    public final boolean remove(Object o) {
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
            Object key = e.getKey();
            Object value = e.getValue();
            return removeNode(hash(key), key, value, true, true) != null;
        }
        return false;
    }
    public final Spliterator<Map.Entry<K,V>> spliterator() {
        return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
    }
    public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
        Node<K,V>[] tab;
        if (action == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            for (Node<K,V> e : tab) {
                for (; e != null; e = e.next)
                    action.accept(e);
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }
}

EntrySet內部類用來表示當前HashMap的鍵集合。

3. 一些工具方法

3.1 hash方法

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

這個hash()方法的(h = key.hashCode()) ^ (h >>> 16)過程值得研究。

首先,h >>> 16是將雜湊值向右無符號移動16位,也就相當於取了原值的高16位。

0000 0100 1011 0011 1101 1111 1110 0001 >>> 16
得到:
0000 0000 0000 0000 0000 0100 1011 0011

然後,我們再來看一個JDK1.7中的indexFor()方法。在JDK8以後,原始碼中也大量出現tab[(n - 1) & hash]的形式,實際上也是一樣的。

static int indexFor(int h, int length) {
    return h & (length-1);
}

indexFor()方法的返回值就是table陣列中我們想要找到的陣列下標。但由於絕大多數情況下length一般都小於2^16即小於65536。所以return h & (length-1)結果始終是h的低16位與(length-1)進行&運算。

所以很顯然的是,我們每次在計算下標的時候,都幾乎只能用到低位。如果我們想一個辦法,把高位也利用起來,那麼就可以增加雜湊的程度。所以hash()方法中選擇了與自身的高十六位(h >>> 16)進行異或,來利用到高位。

3.2 tableSizeFor方法

/**
 * Returns a power of two size for the given target capacity.
 */
static final int tableSizeFor(int cap) {
    int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

tableSizeFor方法用於計算引數cap對應的resize閾值。往往出現為下面的語句。

threshold = tableSizeFor(size);

3.3 resize方法

final Node<K, V>[] resize() {
    // 拿到舊的陣列、容量、擴容閾值
    Node<K, V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {// 如果舊容量不為0
        if (oldCap >= MAXIMUM_CAPACITY) {// 如果舊容量已經達到了最大容量
            // 令擴容閾值為一個不可能達到的最大值
            threshold = Integer.MAX_VALUE;
            // 直接返回舊陣列
            return oldTab;
        } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY //如果舊容量擴大兩倍後仍沒達到最大容量,設其為新容量
                && oldCap >= DEFAULT_INITIAL_CAPACITY)// 並且舊容量大於等於初始容量
            // 令新的擴容閾值擴大為兩倍
            newThr = oldThr << 1;
    } else if (oldThr > 0) // 如果舊容量為0,且舊擴容閾值大於0
        // 那麼就讓新容量變為舊擴容閾值
        newCap = oldThr;
    else { // 舊容量和舊擴容閾值都為0
        // 令新容量為預設初始容量,並計算擴容閾值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }

    if (newThr == 0) {// 如果新擴容閾值為0
        // 計算當前的容量*負載因子
        float ft = (float) newCap * loadFactor;
        // 設定新擴容閾值為ft或Integer.MAX_VALUE
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?
                (int) ft : Integer.MAX_VALUE);
    }
    // 更新擴容閾值
    threshold = newThr;
    // 建立一個新的table,大小為先前計算出來的新容量
    @SuppressWarnings({"rawtypes", "unchecked"})
    Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
    // 更新table指標
    table = newTab;

    if (oldTab != null) {// 如果舊陣列不為null
        for (int j = 0; j < oldCap; ++j) {// 遍歷之
            Node<K, V> e;
            if ((e = oldTab[j]) != null) {// 如果j處的舊陣列值不為null,存進e
                // 將舊陣列裡的值設為null
                oldTab[j] = null;
                if (e.next == null)// 如果e.next為null
                    // 在新陣列中找到並賦值
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)// 如果e這裡是一棵樹,呼叫split分割樹,並帶到新陣列中
                    ((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
                else { // e這裡是連結串列,且e.next不為null
                    // 下面要進行一個連結串列分割操作,將原連結串列分為lo串和hi串兩串。
                    Node<K, V> loHead = null, loTail = null;
                    Node<K, V> hiHead = null, hiTail = null;
                    Node<K, V> next;
                    do {
                        // 儲存next
                        next = e.next;
                        /* 通過e.hash & oldCap是0還是1,可以判斷e應該在原串上還是在新串上。
                           hashMap擴容前後容量大小都是2的冪,假如原容量size二進位制形式是1000,新容量二進位制形式是10000
                           又由於陣列下標位置的計算公式是e.hash & (size-1),那麼原來是hash & 0111,後來是hash & 01111。
                           也就實際上是多用了hash的高一位用來確定位置。那麼hash&size就可以知道hash值中高的這一位是0還是1
                           如果高的一位是0,那麼新位置和原位置一樣,不變。如果高的一位是1,那麼就要移動位置。
                         */
                        if ((e.hash & oldCap) == 0) {// 如果位置不用改變
                            // 將e加入到lo串中
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        } else {// 如果位置需要改變
                            // 將e加入到hi串中
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);// 如果e賦為next後不為null
                    // 將lo串放到下標為j的位置處,即不變的位置
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 將hi串放到下標為j+oldCap處,因為新位置的hash中高一位是1,那下標就要加一個oldCap
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

先嚐試用數學方法確定新容量和新擴容閾值。

隨後遍歷陣列,並深入遍歷其中的連結串列/紅黑樹。如果是連結串列,要將他分裂為lo連結串列和hi連結串列。

3.4 treeifyBin方法

final void treeifyBin(Node<K, V>[] tab, int hash) {
    int n, index;
    Node<K, V> e;
    // 如果陣列很空或很小,那麼就resize()
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {// hash對應的位置在陣列中不為null,就可以進入這個迴圈
        TreeNode<K, V> hd = null, tl = null;
        do {
            // 將節點e轉變為一個樹節點存到變數p
            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);
    }
}

treeifyBin方法將符合要求的連結串列轉變為一個紅黑樹。

4. 一些業務方法

4.1 get方法

public V get(Object key) {
    Node<K, V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K, V> getNode(int hash, Object key) {
    Node<K, V>[] tab;
    Node<K, V> first, e;
    int n;
    K k;
    if ((tab = table) != null // table不為空
        && (n = tab.length) > 0 // table長度不為0
        && (first = tab[(n - 1) & hash]) != null) {// 在算出來的位置拿到的Node不為null,這是連結串列頭
        if (first.hash == hash // 檢查first的雜湊值,假如正確且key也符合
            && ((k = first.key) == key || (key != null && key.equals(k))))
            return first;// 直接返回first
        if ((e = first.next) != null) {// 如果first連結串列長度不止為1
            if (first instanceof TreeNode) {// 如果table當前位置延伸出的是一個紅黑樹
                return ((TreeNode<K, V>) first).getTreeNode(hash, key);// 呼叫getTreeNode方法
            }
            // 不是紅黑樹,是連結串列,遍歷查詢即可
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

詳見上面的註釋。用雜湊值計算出陣列中的位置,再沿著連結串列或紅黑樹去找。

4.2 put方法

public V put(K key, V value) {
    // 呼叫putVal
    return putVal(hash(key), key, value, false, 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;
    if ((tab = table) == null || (n = tab.length) == 0)
        // 如果table為null或者為空,先擴容,再把長度賦值為n
        n = (tab = resize()).length;

    if ((p = tab[i = (n - 1) & hash]) == null)
        // 如果找到的指定位置為空,那麼在這裡用新鍵值對呼叫newNode
        tab[i] = newNode(hash, key, value, null);
    else {
        // 如果找到的位置不為空
        Node<K, V> e;
        K k;
        if (p.hash == hash // 如果雜湊值符合
                && ((k = p.key) == key || (key != null && key.equals(k))))// 並且key也吻合
            // 將當前節點p賦給e
            e = p;
        else if (p instanceof TreeNode)// 如果當前p是一個樹節點,呼叫putTreeVal方法
            e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
        else {// 如果當前是一個連結串列節點
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {// 令e=p.next,如果它為null
                    // 令p.next為用新鍵值對建立的newNode,就是將新節點插在了連結串列尾
                    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,for迴圈一開始的時候e=p.next,所以相當於p後移了
                p = e;
            }
        }
        if (e != null) { // 如果key已經存在,更新它並返回oldValue
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 檢查是否需要擴容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

詳見上面的註釋。用雜湊值計算出陣列中的位置,再在連結串列或紅黑樹中搜尋,做更新操作或newNode操作。

4.3 remove方法

public V remove(Object key) {
    Node<K, V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

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;
    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;
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            // 如果連結串列頭就是,賦值給node
            node = p;
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                // 如果是樹,就呼叫對應的方法找到,賦值給node
                node = ((TreeNode<K, V>) p).getTreeNode(hash, key);
            else {
                // 是連結串列,就遍歷查詢
                do {
                    if (e.hash == hash &&
                            ((k = e.key) == key ||
                                    (key != null && key.equals(k)))) {
                        // 找到了,將當前節點賦值給node
                        node = e;
                        // 退出迴圈,這導致了當找到後,下面的p=e沒有執行
                        break;
                    }
                    // 將當前節點賦值給p
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        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)
                // 如果要移除的是連結串列頭,那麼根據上面do-while塊中的邏輯,node==p,那麼我們直接讓連結串列從node.next開始
                tab[index] = node.next;
            else
                // 如果要移除的不是連結串列頭,那麼根據上面do-while塊中的邏輯node!=p,之前是p->node,刪除node
                p.next = node.next;
            // 處理modCount和size屬性
            ++modCount;
            --size;
            afterNodeRemoval(node);
            // 返回remove掉的節點node
            return node;
        }
    }
    return null;
}

找->刪,非常清晰的邏輯

4.4 clone方法

@Override
public Object clone() {
    HashMap<K,V> result;
    try {
        result = (HashMap<K,V>)super.clone();
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
    result.reinitialize();
    result.putMapEntries(this, false);
    return result;
}

clone()方法對HashMap做了一個淺拷貝。

5. 一些來自JDK8的方法

下面的方法來自於重寫JDK8中的Map介面中的方法。

一些函式式的方法在此不做解析,如compute()merge()等。

5.1 getOrDefault / putIfAbsent

@Override
public V getOrDefault(Object key, V defaultValue) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}

@Override
public V putIfAbsent(K key, V value) {
    return putVal(hash(key), key, value, true, true);
}

很好理解,獲得實際值或預設值/不存在時插入。

5.2 remove / replace

@Override
public boolean remove(Object key, Object value) {
    return removeNode(hash(key), key, value, true, true) != null;
}

@Override
public boolean replace(K key, V oldValue, V newValue) {
    Node<K,V> e; V v;
    if ((e = getNode(hash(key), key)) != null &&
            ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
        e.value = newValue;
        afterNodeAccess(e);
        return true;
    }
    return false;
}

@Override
public V replace(K key, V value) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) != null) {
        V oldValue = e.value;
        e.value = value;
        afterNodeAccess(e);
        return oldValue;
    }
    return null;
}

當對應鍵值對存在時,進行刪除\替換操作。