07-HashMap 源碼解析(集合)

注:源碼系列文章主要是對某付費(fèi)專欄的總結(jié)記錄。如有侵權(quán),請聯(lián)系刪除。

整體架構(gòu)

HashMap 底層的數(shù)據(jù)結(jié)構(gòu)主要是:數(shù)組 + 鏈表 + 紅黑樹。其中當(dāng)鏈表的長度大于 8 時,鏈表就會轉(zhuǎn)化成紅黑樹,當(dāng)紅黑樹的大小小于 6 時,紅黑樹會轉(zhuǎn)化成鏈表,整體的數(shù)據(jù)結(jié)構(gòu)如下:

HashMap 數(shù)據(jù)結(jié)構(gòu)圖例

圖中左邊豎著的是 HashMap 的數(shù)組結(jié)構(gòu) table,數(shù)組的元素可能是單個 Node,也可能是個鏈表,也可能是個紅黑樹,比如數(shù)組下標(biāo)索引為 1 的位置就是一個鏈表,下標(biāo)索引為 8 的位置對應(yīng)的就是紅黑樹,具體細(xì)節(jié)下文繼續(xù)。

1.1 類注釋

從 HashMap 的類注釋中,我們可以得到如下信息:

  • 允許 null 值(作為鍵或值),不同于 HashTable,是線程不安全的;
  • load factor(影響因子)默認(rèn)值是 0.75,是均衡了時間和空間損耗算出來的值,較高的值會減少空間開銷(擴(kuò)容減少,數(shù)組大小增長速度變慢),但增加了查找成本(hash 沖突增加,鏈表長度變長),不擴(kuò)容的條件:數(shù)組容量 > 需要的數(shù)組大小 / load factor
  • 如果有很多數(shù)據(jù)需要存儲到 HashMap 中,建議 HashMap 的容量一開始就設(shè)置成足夠的大小,這樣可以防止其過程中不斷的擴(kuò)容,影響性能;
  • HashMap 是非線程安全的,我們可以自己在外部加鎖,或者通過 Collections#synchronizedMap 來實(shí)現(xiàn)線程安全,Collections#synchronizedMap 的實(shí)現(xiàn)是在每個方法上都加上了 synchronized 鎖;
  • 在迭代過程中,如果 HashMap 的結(jié)構(gòu)被修改,會快速失敗。

1.2 常見屬性

// 初始容量默認(rèn)值為 16,必須是 2 的冪次方
/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

// 最大容量默認(rèn)值,必須是 2 的冪次方并且小于等于 1 << 30
/**
 * 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;

// 負(fù)載因子默認(rèn)值
/**
 * The load factor used when none specified in constructor.
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 桶上的鏈表長度大于等于 8 時,鏈表轉(zhuǎn)化為紅黑樹
/**
 * 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;

// 桶上的紅黑樹大小小于等于 6 時,紅黑樹轉(zhuǎn)化為鏈表
/**
 * The bin count threshold for untreeifying a (split) bin during a
 * resize operation. Should be less than TREEIFY_THRESHOLD, and at
 * most 6 to mesh with shrinkage detection under removal.
 */
static final int UNTREEIFY_THRESHOLD = 6;

// 當(dāng)數(shù)組容量大于 64 時,鏈表才會轉(zhuǎn)化為紅黑樹
/**
 * The smallest table capacity for which bins may be treeified.
 * (Otherwise the table is resized if too many nodes in a bin.)
 * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
 * between resizing and treeification thresholds.
 */
static final int MIN_TREEIFY_CAPACITY = 64;

// 存放數(shù)據(jù)的數(shù)組
transient Node<K,V>[] table;

transient Set<Map.Entry<K,V>> entrySet;

// HashMap 的實(shí)際大小
transient int size;

// 記錄迭代過程中 HashMap 結(jié)構(gòu)是否發(fā)生變化,如果有變化,迭代會 fail-fast
transient int modCount;

// 擴(kuò)容的門檻,有兩種情況:
// 初始化時指定了數(shù)組大小的話,則通過 tableForSize 方法計(jì)算,數(shù)組的大小為 2 的冪次方
// 如果是通過 resize 方法進(jìn)行擴(kuò)容,大小 = 數(shù)組容量 * 0.75
int threshold;

// 負(fù)載因子
// 空參構(gòu)造器時,賦值為默認(rèn)的負(fù)載因子 0.75
final float loadFactor;

// 鏈表的節(jié)點(diǎn)
static class Node<K,V> implements Map.Entry<K,V> {}

// 紅黑樹的節(jié)點(diǎn)
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {}

2 新增

新增源碼流程圖:

HashMap put 流程.png

源碼分析:

// hash: 通過 hash 算法計(jì)算出來的值
// onlyIfAbsent: false 表示即使 key 已經(jīng)存在了,仍然會用新值覆蓋原來的值,默認(rèn)為 false
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    // tab 表示數(shù)組,n 表示數(shù)組的長度,i 表示數(shù)組索引下標(biāo),p 為 i 下標(biāo)位置的 Node 值
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 如果數(shù)組為空,使用 resize 方法初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 如果當(dāng)前索引位置是空的,直接生成新的節(jié)點(diǎn)在當(dāng)前索引位置上
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 如果當(dāng)前索引位置有值的處理方法,即我們常說的如何解決 hash 沖突
    else {
        // e: 當(dāng)前節(jié)點(diǎn)的臨時變量
        Node<K,V> e; K k;
        // 如果新增的 key 的 hash 和值都相等,則直接把當(dāng)前下標(biāo)位置的 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 是鏈表的尾節(jié)點(diǎn)
                if ((e = p.next) == null) {
                    // 如果 p 是鏈表的尾節(jié)點(diǎn),則直接將新節(jié)點(diǎn)放到鏈表的尾部
                    p.next = newNode(hash, key, value, null);
                    // 當(dāng)鏈表的長度大于等于 8 時,鏈表轉(zhuǎn)化為紅黑樹
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 鏈表遍歷過程中,發(fā)現(xiàn)有元素和新增的元素相等,結(jié)束循環(huán)
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 說明新節(jié)點(diǎn)的新增位置已經(jīng)找到了
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // 當(dāng) onlyIfAbsent 為 false 時,才會覆蓋值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            // 返回舊的值
            return oldValue;
        }
    }
    // 記錄 HashMap 的數(shù)據(jù)結(jié)構(gòu)發(fā)生了變化
    ++modCount;
    // 如果 HashMap 的實(shí)際大小大于擴(kuò)容的門檻,則開始擴(kuò)容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

新增的流程上面已經(jīng)表示很清楚了,接下來看看鏈表和紅黑樹的新增。

2.1 鏈表的新增

鏈表的新增比較簡單,就是把當(dāng)前節(jié)點(diǎn)追加到鏈表的尾部,和 LinkedList 的追加實(shí)現(xiàn)一樣。

當(dāng)鏈表長度大于等于 8 時,此時鏈表就會轉(zhuǎn)化為紅黑樹,轉(zhuǎn)化的方法是 treeifyBin,此方法有一個判斷,當(dāng)鏈表長度大于等于 8,并且整個數(shù)組大小大于 64 時,才會轉(zhuǎn)換為紅黑樹,當(dāng)數(shù)組大小小于 64 時,只會觸發(fā)擴(kuò)容,不會轉(zhuǎn)化為紅黑樹。

可能面試的時候,有人問你為什么是 8,這個答案在源碼注釋中有說,翻譯大概如下:

鏈表查詢的時間復(fù)雜度是 O(n),紅黑樹的查詢復(fù)雜度是 O(log(n))。在鏈表數(shù)據(jù)不多的時候,使用鏈表進(jìn)行遍歷也比較快,只有當(dāng)鏈表數(shù)據(jù)比較多的時候,才會轉(zhuǎn)化為紅黑樹,但紅黑樹需要占用的空間是鏈表的兩倍,拷貝到轉(zhuǎn)化時間和空閑損耗,所以我們需要定義出轉(zhuǎn)化的邊界值。

在考慮設(shè)計(jì) 8 這個值時,參考了<a >泊松分布概率函數(shù)</a>,由泊松分布中得出結(jié)論,鏈表各個長度的命中概率為:

* 0:    0.60653066
* 1:    0.30326533
* 2:    0.07581633
* 3:    0.01263606
* 4:    0.00157952
* 5:    0.00015795
* 6:    0.00001316
* 7:    0.00000094
* 8:    0.00000006

意思是,當(dāng)鏈表的長度是 8 的時候,出現(xiàn)的概率是 0.00000006,不到千萬分之一,所以說正常情況下,鏈表的長度不可能到達(dá) 8,而一旦達(dá)到 8 時,肯定是 hash 算法出了問題,所以在這種情況下,為了讓 HashMap 仍然有比較高的查詢性能,所以讓鏈表轉(zhuǎn)化為紅黑樹,我們正常些代碼,使用 HashMap 時,幾乎不會碰到鏈表轉(zhuǎn)化為紅黑樹的情況,畢竟概率只有千萬分之一。

2.2 紅黑樹的新增

略。

3 查找

HashMap 的查找分為以下三步:

  1. 根據(jù) hash 算法定位數(shù)組的索引位置,equals 判斷當(dāng)前索引處節(jié)點(diǎn)是否是我們需要尋找的 key,是的話直接返回,不是的話繼續(xù)往下;
  2. 判斷當(dāng)前節(jié)點(diǎn)有無 next 節(jié)點(diǎn),有的話判斷是鏈表類型,還是紅黑樹類型;
  3. 分別走鏈表和紅黑樹不同類型的查找方法。

源碼如下:

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;
    // 只有當(dāng)數(shù)組不為空,并且數(shù)組長度大于0,并且根據(jù)當(dāng)前查找key的hash得到的節(jié)點(diǎn)不為null才進(jìn)入查找
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 如果當(dāng)前節(jié)點(diǎn)的就是我們要查找的節(jié)點(diǎn)則直接返回
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 如果當(dāng)前節(jié)點(diǎn)的下一個節(jié)點(diǎn) next 不為空則繼續(xù)
        if ((e = first.next) != null) {
            // 判斷當(dāng)前節(jié)點(diǎn)是紅黑樹則走紅黑樹查找
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 判斷當(dāng)前節(jié)點(diǎn)是鏈表
            // 采用自旋方式從鏈表中查找 key,e 初始化鏈表的頭節(jié)點(diǎn) first 的下一個節(jié)點(diǎn) next
            do {
                // 如果當(dāng)前節(jié)點(diǎn) hash 等于 key 的 hash,并且 equals 相等,則當(dāng)前節(jié)點(diǎn)就是我們要找的節(jié)點(diǎn)
                // 當(dāng) hash 沖突時,同一個 hash 值上是一個鏈表的時候,我們通過 equals 方法來比較 key 是否相等的
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            // 否則,把當(dāng)前節(jié)點(diǎn)的下一個節(jié)點(diǎn)拿出來繼續(xù)尋找
            } while ((e = e.next) != null);
        }
    }
    return null;
}

紅黑樹的查找:略。

總結(jié)

HashMap 的內(nèi)容雖然比較多,但大多數(shù) API 都只是針對數(shù)組 + 鏈表 + 紅黑樹這種數(shù)據(jù)結(jié)構(gòu)進(jìn)行封裝而已。

------------------------------------- END -------------------------------------

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容