HashMap源碼分析(II)

上一節主要講述了HashMap的一些基礎屬性和構造方法,本節將會講述HashMap的核心方法。
put方法
  
    
  
  
   
   
            
   
   
  1. node

  2. 數組

  3. 微信


  4. app

  5. 性能

  6. flex

  7. 優化

  8. this

  9. spa

  10. .net

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;}
  1. 首先查看當前的bucket集合是否爲空,若是爲空則進行擴容,具體的擴容邏輯後面詳細說

  2. 計算key存儲在bucket的位置,並判斷當前bucket位置上有沒有數據節點,若是沒有則新建一個

  3. 若是bucket位置上存在數據節點,若是數據節點的hash值等於新數據的hash,而且數據節點的key和新數據的key相等,最終只是用新節點的Value替換原來節點Value(onlyIfAbsent爲false的前提下)

  4. 若是不知足3的條件,而且Node是個TreeNode的話,那麼將採用紅黑樹的插入方式增長節點

  5. 若是不知足條件3而且節點不是一個TreeNode,則須要循環遍歷鏈表中的節點,若是鏈表中的節點有和新的數據相等的節點,則直接退出,而且用新節點的Value替換原來節點的Value(onlyIfAbsent爲false的前提下)

  6. 當遍歷到鏈表節點的最後一個節點時,將該節點的下一個節點賦值爲新的數據節點,而且判斷是否已經知足樹狀化的條件,若是是則須要bucket中的鏈表進行樹狀化,除了知足TREEIFY_THRESHOLD須要還有一個條件才能進行樹狀化,那就是整個bucket的數量須要知足64

  7. 當新增完節點之後,會判斷數量是否已經超過threshold,若是超過了就須要進行擴容

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 && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null;}
  1. 根據key值計算出來的hash找到bucket的位置,先比較第一個節點,若是key相等直接返回該節點

  2. 若是key不相等,而後判斷節點若是是TreeNode,則須要遍歷樹找到對應的節點

  3. 若是節點不是TreeNode,則遍歷列表找到key相等的節點

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 = p; else if ((e = p.next) != null) { if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } 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) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null;}
  1. 首先仍是計算bucket中的位置,若是bucket中的第一個節點相等,那麼就須要把bucket中的元素替換爲原來的節點的下一個節點

  2. 若是bucket中的第一個節點不相等,那麼就須要根據Node類型來遍歷樹或者鏈表查找相等的節點

  3. 找到相等的節點之後,若是是樹狀化的節點,就將樹中的節點移除,若是是鏈表,就須要將刪除的節點的next值更改成要刪除的的節點的next的值

  4. 最後修改size,也就是整個hashmap中節點的數量

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) { 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. 首先要作的事計算cap和threshold的值,新的容量計算須要根據老的threshold,若是擴容是第一次,那麼第一次擴容的的容量就是在初始化map時指定的容量值(initalCapacity),若是不是第一次擴容,擴容後的容量就是擴容前的threshold值

  2. threshold的值的計算規則是,若是容量已經超過了16,那麼新的threshold直接在原來的基礎上double,不然就是用新的容量*負載因子

  3. 將計算好的threshold進行賦值

  4. 擴容時首先準備好一個新容量的Node集合。

  5. 而後遍歷bucket,取出bucket的節點,若是取出的bucket中的節點只有一個節點那麼直接從新hash放入新的bucket中

  6. 若是不是,而且節點是一個樹狀節點那麼就利用樹節點的擴容邏輯而且將節點移到合適的位置

  7. 若是是鏈表這邊JDK8也是作了優化,減小了從新hash的性能消耗,而是採用了一個簡單的邏輯,當節點的hash值和原來的容量作與運算時若是結果爲0,表示該節點仍是在原來的位置,若是不是,那麼就是在原來的位置再加+原來的容量的位置。


下面講一下爲何用(e.hash & oldCap)==0就能夠判斷元素的位置,首先按照原來的計算位置的方法都是按照hash值和length-1進行與運算取值的,而length又都是2的N次冪,那麼length-1說明低位全是1,新容量只不過是將老的容量進行左移一位,若是(e.hash & oldCap) == 0說明與新老length-1的計算結果是同樣的,因此就在原位置,不然就是老位置+原數組長度。

本文分享自微信公衆號 - shysh95(shysh95)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索