HashMap原始碼剖析

linzeliang發表於2021-09-24

1. HashMap繼承結構

image-20210924204750139

2. HashMap底層資料結構

在1.7及其之前,HashMap底層是使用 陣列 + 連結串列實現的,在1.8及其之後,使用了 陣列 + 連結串列/紅黑樹 實現。

來看下1.7的儲存結構圖:

image-20210924200421961

其中連結串列使用內部類Node來實現的:

image-20210924205026504

陣列+連結串列(雜湊表) 其實就是用於解決雜湊衝突使用的一個拉鍊法方法。在資料結構中,我們處理hash衝突常使用的方法有:開發定址法、再雜湊法、鏈地址法、建立公共溢位區。而HashMap中處理hash衝突的方法就是鏈地址法。

但是這樣子的話,如果使用了很久,HashMap儲存的元素越來越多,那麼連結串列就會變的很長,那麼效能就會下降很多(因為連結串列不適合查詢元素,每次查詢元素都要從頭開始遍歷)。

於是在1.8的時候進行了改進,使用到了紅黑樹(紅黑樹是一個自平衡的二叉查詢樹,查詢效率是非常高,時間複雜度僅為O(logN))。

image-20210924203138870

在HashMap中,連結串列轉化成紅黑樹的條件是當連結串列長度大於8陣列(桶)的個數要大雨等於64個時,才可以將連結串列轉化成紅黑樹,它們在原始碼中的定義如下:

static final int MIN_TREEIFY_CAPACITY = 64; // 轉化成紅黑樹的最小的桶容量
static final int TREEIFY_THRESHOLD = 8; // 桶上的元素的數量

treeifyBin中的片段:

// 意思是隻要桶的個數小於64個,那麼即使桶中的元素個數超過了8個,那麼就進行resize擴容,而不是轉化成紅黑樹
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)  
    resize();

putVal中的片段:

if ((e = p.next) == null) {
    p.next = newNode(hash, key, value, null);
    // -1 for 1st 可以理解為元素下表從-1開始的,所以可以看作binCount >= 9
    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
        treeifyBin(tab, hash);
    break;
}

3. HashMap的屬性

// 預設的初始容量,左移位4位相當於:1*2*2*2*2=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大的容量:2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
// 預設裝載因子為0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 當一個元素被新增到至少有8個節點的桶中,桶中的連結串列將會被轉化成紅黑樹,即轉化成紅黑樹條件是大於8個
static final int TREEIFY_THRESHOLD = 8;
// 紅黑樹退化成連結串列的條件:小於等於6時退化
static final int UNTREEIFY_THRESHOLD = 6;
// 轉化成紅黑樹的最小的桶的數量
static final int MIN_TREEIFY_CAPACITY = 64;

成員屬性有如下:

image-20210924204641256

4. 構造方法

一共有4個構造方法:

image-20210924213501347

其中,核心的構造方法是:

public HashMap(int initialCapacity, float loadFactor) {
    // 保證初始容量大於等於0,否則丟擲異常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    // 保證初始容量不大於最大容量,超過了就講初始容量設定為最大容量
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // 保證裝載因子大於0
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
    // 初始化裝載因子為0.75
    // 當桶中元素到達8個的時候,概率已經變得非常小,也就是說用0.75作為載入因子,每個碰撞位置的連結串列長度超過8個是幾乎不可能的。當桶中元素到達8個的時候,概率已經變得非常小,也就是說用0.75作為載入因子,每個碰撞位置的連結串列長度超過8個是幾乎不可能的。
    this.loadFactor = loadFactor;
    // threshold這個成員變數是閾值,決定了是否要將雜湊表再雜湊,它的值應該是:capacity * load factor
    // 但是這裡的threshold並不是真正的初始化閾值,正在的初始化閾值時在resize的時候進行初始化(而此時的threshold並不是沒有用,而是待會在初始化容量時候要用的初始值)
    this.threshold = tableSizeFor(initialCapacity);
}

在初始化閾值容量的時候,呼叫了tableSizeFor方法:

// 這個方法返回大於輸入數字的最近的2的整數次冪的數
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

5. put方法

put方法其實是呼叫了putVal方法的,呼叫方法的同時把計算好的key的雜湊值傳入,putVal方法:

public V put(K key, V value) {
    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)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        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))))
            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
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

put的過程如下

Node<K,V>[] tab; // tab表示的是雜湊陣列
Node<K,V> p; // p表示的是陣列的第一個節點
Node<K,V> e; // e表示該key是否已經存在,為null表示不存在
  1. put方法接收傳入key與value:put(K key, V value)

  2. 計算出key的雜湊值,這裡計算的雜湊值方法是key的hashcode與hashcode的高16位進行異或運算得到的結果

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
  3. 將計算得到的雜湊值、key、value傳給putVal方法

  4. 在putVal方法中,先判斷雜湊陣列是否為空,如果為空的話就resize初始化tab,建立新的陣列

    // 判斷tab是否為空
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    
  5. 如果存在雜湊表,則計算key對應的索引位置:p = tab[i = (n - 1) & hash,使用length-1hash進行邏輯與運算(因為在做&運算的時候,僅僅是後4位有效,那麼如果key的雜湊值低位變化不大,高位變化大,那麼在計算的時候發生雜湊衝突的可能性也增大許多,所以上面在計算雜湊的時候將hash與hash的高16為進行異或運算得到結果作為雜湊值,增加了隨機性),如果改索引位置還沒有節點,那麼就直接插入到該位置即可!

    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    
  6. 如果該桶上有元素的話,就根據該桶的結構是紅黑樹還是連結串列進行插入,然後返回結果賦值給e

    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;
        }
    }
    
  7. 如果e是為null,就說明該key不存在,直接插入,如果不為null,說明key已經存在,直接將覆蓋原來的value,並返回

  8. 插入成功之後,還要判斷一下實際存在的鍵值對的數量size是否大於閾值threshold,如果大於那麼就擴容

6. 擴容

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) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
  1. 先判斷原來的容量是否大於0

  2. 如果大於0的話且大於等於最大容量,就將閾值設定為Integer.MAX_VALUE,然後啥也不幹

    如果大於0的話且小於於最大容量就將舊的容量擴容為原來的兩倍,同時也將舊的閾值擴大為原來的兩倍

    if (oldCap > 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; // double threshold
    }
    
  3. 如果初始容量未制定或者小於等於0(就是HashMap構造方法的那種情況,只初始化了threshold閾值),那麼就將閾值作為初始化容量(此時閾值是2的整數次冪,HashMap的容量要為2的整數次冪)

    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    
  4. 剩下的情況就是初始容量沒有設定,閾值也沒有設定,那麼容量就用預設的DEFAULT_INITIAL_CAPACITY,閾值則為:(int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)

    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    
  5. 如果新容量的閾值為設定,那麼就設定下:

    if (newThr == 0) {
          float ft = (float)newCap * loadFactor;
          newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                          (int)ft : Integer.MAX_VALUE);
    }
    
  6. 重新整理當前容量的閾值

    threshold = newThr;
    
  7. 最後就是將舊的資料複製到新陣列裡面,有兩種情況:

    1. 擴容後,若hash值新增參與運算的位=0,那麼元素在擴容後的位置=原始位置
    2. 擴容後,若hash值新增參與運算的位=1,那麼元素在擴容後的位置=原始位置+擴容後的舊位置

    擴容後長度為原hash表的2倍,於是把hash表分為兩半,分為低位和高位,如果能把原連結串列的鍵值對, 一半放在低位,一半放在高位,而且是通過e.hash & oldCap == 0來判斷。因此有50%的概率放在新hash表低位,50%的概率放在新hash表高位。

7. get方法

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

get方法的實現就是計算key的hash值,然後通過getNode獲取對應的value

8. remove方法

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

remove方法也是通過計算key的hash,呼叫removeNode來刪除元素的

9. HashMap的一些特性

  • 允許key和value為null
  • 除了允許為努力了和同步,其他的和HashTable一樣
  • 不保證有序
  • 初始容量太高或者太低對便利都不太好
  • 當雜湊表容量超過初始容量*裝載因子時,雜湊表會進行再散裂,桶數量*2
  • 不同步,想要同步可以使用Collections工具類實現Map m = Collections.synchronizedMap(new HashMap(...));
  • 裝載因子預設是0.75,設定高雖然會減少空間,但是遍歷的開銷會增加。因此在設定初始容量時,應該考慮好裝載因子和容量的大小,如果設定的好,就不用再散裂了

相關文章