HashMap 源碼解析2、put 相關函數

HashMap 源碼解析3、get 相關函數以及常見的面試題html

HashMap put 相關函數

文接上回,咱們講了HashMap 的構造函數,主要就是設置 負載因子 和 擴容閾值。 這章咱們來看HashMap put 的相關函數,很少bb,上源碼:java

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    @Override
    public V putIfAbsent(K key, V value) {
        return putVal(hash(key), key, value, true, true);
    }
    
    public void putAll(Map<? extends K, ? extends V> m) {
        putMapEntries(m, true);
    }
   
複製代碼

put 相關的函數就這3 個,前兩個都是直接調用的
putVal(hash(key), key, value, true, true),
後面一個應該還有印象,跟參數爲Map構造函數調用的是同一個函數putMapEntries()。 接着咱們就看下putVal(hash(key), key, value, true, true) 這個函數面試

putVal(hash(key), key, value, true, true) 函數

transient Node<K,V>[] table;

    /** * 在put 相關方法中被調用 * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */
    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)//註釋1
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null) //註釋2
            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;//註釋3
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
複製代碼
第一次調用時

假設咱們是經過HashMap()這個構造函數建立的HashMap 對象並第一次調用 put(K key, V value) 函數。markdown

  1. 咱們先看下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;
        }
        //省略代碼。。。
    }
複製代碼

能夠看出這是一個單鏈表結構,存放着 hash、key和valueapp

  1. resize() 函數,做用:初始化或者擴容表爲原大小的2倍。源碼後面再分析。
  2. 知道以上的信息咱們在看 putVal() 函數的代碼,註釋1
if ((tab = table) == null || (n = tab.length) == 0)//註釋1
    n = (tab = resize()).length;
複製代碼

咱們知道DEFAULT_INITIAL_CAPACITY = 1 << 4 // aka 16 所以能夠知道
tab = (Node<K,V>[])new Node[16] n = 16ide

  1. 接着往下看,註釋2
if ((p = tab[i = (n - 1) & hash]) == null) //註釋2
    tab[i] = newNode(hash, key, value, null);
複製代碼

咱們是第一次調用,p = tab[i = (n - 1) & hash]確定是null ,因而咱們此次就成功的把key,value 存到了tab[i] 中。函數

  1. 咱們走進了if,else 的代碼就不用看了,直接到了//註釋3 的位置 ++modCount 用於記錄修改的次數,接着往下看:
if (++size > threshold)
    resize();
複製代碼

threshold爲擴容閾值,初始化時爲 DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR //aka 16*0.75 = 12, size 爲HashMap 中保存 Node的數量,當等於 擴容閾值 時就須要對 tab 進行擴容。
接着往下是 afterNodeInsertion(evict);這是一個空方法,什麼都沒作。oop

void afterNodeInsertion(boolean evict) { }
複製代碼

好了,咱們第一次調用就結束了。成功的把數據存儲到了HashMap 中,再回頭看下咱們以前沒有看的 else 中的狀況post

else 中的狀況

tab[i = (n - 1) & hash]中已經有值的狀況就會走到 else 中,看代碼:性能

static final int TREEIFY_THRESHOLD = 8;
        
        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 //註釋4
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
複製代碼
  1. TreeNode 爲紅黑樹,有興趣的同窗能夠自行了解 紅黑樹深刻剖析及Java實現

  2. 咱們看接下來的判斷,能夠分爲3中狀況

    • p.key 與 參數key 相同

    直接將p 賦值給 Node e

    • p 爲 TreeNode

    將須要保存的內容 添加到紅黑樹p 中,返回值賦給e

    • else

    遍裏鏈表p,且結果賦值給e; 也可分爲3 中狀況
    1). if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) 當鏈表中有Node 的key 與參數key 相同時,結束遍歷。
    2). if ((e = p.next) == null) 鏈表遍歷完時
    將須要保存的內容 添加到鏈表末尾。若是鏈表長度小於8,結束遍歷
    3). 鏈表遍歷完,且添加新增內容。鏈表長度大於等於8 時。
    執行 treeifyBin(tab, hash) 函數,而後結束遍歷。

    static final int MIN_TREEIFY_CAPACITY = 64;
    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);
        }
    }
    複製代碼

    treeifyBin(tab, hash) 函數的邏輯是當tab 長度小於64 時就執行resize() 擴容,不然將鏈表轉爲紅黑樹

  3. 咱們會過來看註釋4 處代碼

if (e != null) { //當參數key 在tab 中有映射時 
    V oldValue = e.value;
    if (!onlyIfAbsent || oldValue == null)
        e.value = value;
    afterNodeAccess(e);
    return oldValue;
}
複製代碼

onlyIfAbsent 爲true 時不修改現有值
當參數key 在tab 中有映射時,根據條件覆蓋現有值,並返回舊值。

int hash(Object key) 函數

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
複製代碼
  1. ^ 若是相對應位值相同,則結果爲0,不然爲1, 若是相對應位都是1,則結果爲1,不然爲0
a     = 1010
b     = 0011
------------
a ^ b = 1001
a & b = 0010
複製代碼
  1. (h = key.hashCode()) ^ (h >>> 16) 這個方法最重要的一句代碼。爲何要作 ^ (h >>> 16)這個操做呢?

是由於在putVal 函數中,是這樣是使用的 tab[index = (n - 1) & hash],n 是表的長度,表的長度永遠都是2的冪次方,那麼n-1的高位應該全是0,作 & 操做時會致使hash 的高位沒法參與運算,從而會帶來哈希衝突的風險。因此在計算key的哈希值的時候,作(h = key.hashCode()) ^ (h >>> 16)操做。這也就讓高位參與到tab[index = (n - 1) & hash]的計算中來了,即下降了哈希衝突的風險又不會帶來太大的性能問題。

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) {//table中已經有數據
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // newCap = oldCap * 2
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; //newThr = oldThr * 2 
        }
        else if (oldThr > 0) //初始化是設置了容量和閾值 使用非空構造函數初始化
            //回顧一下構造函數中 threshold = tableSizeFor(initialCapacity) 值爲2的冪次方
            //原來這個值實際上是給newCap 因此須要爲2的冪次方
            // 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) {// 上面判斷進入 (oldThr > 0) 的狀況,沒有給newThr 賦值,
            // 因此在這裏給newThr 賦值
            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) {//註釋5
                                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;//註釋6
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;//註釋7
                        }
                    }
                }
            }
        }
        return newTab;
    }
複製代碼

resize() 函數能夠分爲兩個部分

  • 設置 newCap、newThr。分析已經寫在了代碼中,邏輯就是:沒有初始化的狀況初始化、已經有值的乘以2
  • 建立newTab 並從新賦值。這裏咱們重點解釋一下 if ((e.hash & oldCap) == 0)註釋5 處的代碼。
  1. 首先咱們知道 tab[index = (n - 1) & hash],假設oldCap = 16 時
oldCap-1  = ... 0000 1111
hash1     = ... 0000 0101  -> index1 = 0000 0101 = 5
hash2     = ... 0001 0101  -> index2 = 0000 0101 = 5
複製代碼
  1. 此時須要擴容 newCap = oldCap*2 = 32
newCap-1  = ... 0001 1111
hash1     = ... 0000 0101  -> index1 = 0000 0101 = 5
hash2     = ... 0001 0101  -> index2 = 0001 0101 = 5 + 16(oldCap)
複製代碼
  1. 從以上兩步能夠看出,在擴容的時候並非全部index 都會改變的。而改變的關鍵就是在 hash 值在 oldCap 的位上是否爲0。if ((e.hash & oldCap) == 0)註釋5 處的代碼就相似於一下列子。做用就是判斷是否須要位移。
oldCap    = ... 0001 0000
hash1     = ... 0000 0101  -> 0
hash2     = ... 0001 0101  -> 1
複製代碼
  1. 這也解釋了註釋六、註釋7處的代碼

小結

這章咱們主要涉及到3個函數

  • putVal(...)

該函數負責數據插入,當size 超過擴容閾值時調用resize()函數擴容,當添加數據的鏈表長度大於等於8 時將鏈表轉爲紅黑樹。

  • hash(Object key)

在計算key的哈希值的時候,用其自身hashcode值與其低16位作異或操做。這也就讓高位參與到index的計算中來了,即下降了哈希衝突的風險又不會帶來太大的性能問題。

  • resize()

從新設置table 的容量和擴容閾值,並新建table 把oldTable 的值填充進去。

相關文章
相關標籤/搜索