HashMap源碼分析

參考:JDK1.8源碼

開場白:Hashtable是遺留類,很多映射的常用功能與HashMap類似,不同的是它承自Dictionary類,並且是線程安全的,任一時間只有一個線程能寫Hashtable,併發性不如ConcurrentHashMap,因爲ConcurrentHashMap引入了樂觀鎖(JDK1.7是分段鎖)。Hashtable不建議在新代碼中使用,不需要線程安全的場合可以用HashMap替換,需要線程安全的場合可以用ConcurrentHashMap替換

從結構實現來講,HashMap是數組+鏈表+紅黑樹(JDK1.8增加了紅黑樹部分)實現的

HashMap繼承圖

HashMap

HashMap構造方法

  public HashMap()
  public HashMap(int initialCapacity)
  public HashMap(int initialCapacity, float loadFactor)
  public HashMap(Map<? extends K, ? extends V> m) 

一些成員變量的理解

int initialCapacity;//初始化容量  
final float loadFactorloadFactor;//負載因子(擴容時,用來與容量相乘)  
int threshold;//閥值(容量大於該值就會擴容,擴大爲原來的兩倍,但最大不超過MAXIMUM_CAPACITY)
transient int size://容量值
transient Node<K, V>[] table;//實際裝載數據的數組,類型爲Node<K, V>

構造方法源碼:

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)//最大容量爲MAXIMUM_CAPACITY
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);//保證容量數值是2的倍數
}
//無參構造方法只是設置了負載因子loadFactor
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

tableSizeFor源碼:

    /**
     * 返回大於等於cap的,最小的,同時是2的n次方的數,最大不超過MAXIMUM_CAPACITY
     */
    static final int tableSizeFor(int cap) {
        //假設n=5,那最後應該返回8
        int n = cap - 1; //n=4
        n |= n >>> 1; // n = 0100 | 0010 = 0110
        n |= n >>> 2; // n = 0110 | 0001 = 0111
        n |= n >>> 4; // n = 0111 | 0000 = 0111
        n |= n >>> 8; // n = 0111 | 0000 = 0111
        n |= n >>> 16;// ...
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

之所以要保證容量數值爲2的倍數,是爲了優化對象根據hash值求其所在數組下標的速度。例如容量size爲4(二進制爲100),當新增一個數據是,HashMap會根據key的hash值和(size-1)相與(即hash&011),那得到的結果肯定不會大於3,所以不會造成數組越界,而且&比%具有更高的效率。

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

Node類結構很簡單,字段有4個,hash值、key值、value值、還有一個Node類型的next字段用於指向下一個節點,從而實現鏈表結構。

HashMap重要方法

HashMap如何添加數據,如何擴容的呢?答案在putVal和resize方法裏。
先看添加數據putVal()方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K, V>[] tab;
    Node<K, V> p; //tab裏的計算出的下標的Node數據
    int n, i;
    if ((tab = table) == null || (n = tab.length) == 0) {
        //如果調用的是無參構造方法,table爲空,所以進行擴容
        n = (tab = resize()).length;
    }
    if ((p = tab[i = (n - 1) & hash]) == null) {
        //計算出的下標下沒有數據,就新建一個Node加進去
        tab[i] = newNode(hash, key, value, null);
    }
    else {
        //這是HashMap裏面已經有一個同樣的key的情況
        Node<K, V> e;//存放最終找到的鍵值key的Node對象
        K k;
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))){
            //該key下只有一個數據的情況
            e = p;
        }
        //下面是hash衝突的情況,分兩種,一種是用紅黑樹結構存,一種是用鏈表存。
        //從下面代碼可以看到同一個下標下數據大於TREEIFY_THRESHOLD(8)時會把鏈表變成紅黑樹結構
        else if (p instanceof TreeNode) {
            //如果p是紅黑樹,就用紅黑樹的添加方法
            e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
        }
        else {

            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    //找不到同hash值同key的數據,新建一個
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // 改爲紅黑樹結構存
                        treeifyBin(tab, hash);
                    break;
                }
                //找到了同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;
    if (++size > threshold) {
        //進行擴容
        resize();
    }
    afterNodeInsertion(evict);
    return null;
}

上面可以看到容量超過閾值就會進行擴容,下面看擴容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) {//當前map數據量大於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) { //HashMap創建後第一次擴容
        newCap = oldThr;
    }
    else {//HashMap創建後第一次擴容,調用無參構造方法,oldThr會爲0,賦值新的容量爲16  閥值爲16*0.75=12
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float) newCap * loadFactor;
        //如果大於MAXIMUM_CAPACITY(60)就賦值爲Integer.MAX_VALUE
        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;//把舊數組裏的引用置爲null,才能讓虛擬機回收掉
                if (e.next == null) {// 該key值下只有一個數據,用舊的hash與新長度-1的值&,作爲下標
                    newTab[e.hash & (newCap - 1)] = e;
                }
                else if (e instanceof TreeNode) {//紅黑樹的插入
                    ((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
                }
                else { // 鏈表結構,下面具體分析這段代碼
                    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;
}

來分析下複製鏈表結構那段代碼

那裏把hash值與oldCap的值爲0的分爲一條鏈表,不等於0也分爲一條鏈表,最後把等於0的鏈表放到原來的下標j裏,不等於0的放在j+oldCap下標裏。爲什麼這麼做的?
我們知道求數據所在的下標,是通過key的hash值&(oldCap-1),而那裏是hash&oldCap。
舉個例子,原來容量爲oldCap爲4(二進制0100),有兩個數據key的hash值分別爲1,5,那麼根據hash&(oldCap-1)求其下標,1&0011=1,101&011=1,那它們都會放在數據下標爲1那。
當擴容了,新的容量newCap爲8(爲原來的2倍),根據hash&(newCap-1)求其下標,1 & 0111=1,101 & 0111=101=5,可以看到hash值1還是放在了下標爲1那,hash值爲5的放在了下標爲5的地方,剛好是1+oldCap。

歸根到底還是HashMap把容量設置成2的次方數,擴容策略爲原來容量的2倍的前提條件,使得容量size會是001000…的形式,容量size-1低位會是1111這樣的形式,擴容爲2倍,實際就把數左移一位,新的容量值newCap-1,會比原來容量值oldCap-1多一位1,那麼hash & oldCap是求hash值在多出的那一位是0還是1,如果是1,複製後的下標是原來下標+oldCap

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