面試必備:HashMap源碼解析(JDK8)

 

1 概述

本文將從幾個經常使用方法下手,來閱讀HashMap的源碼。 
按照從構造方法->經常使用API(增、刪、改、查)的順序來閱讀源碼,並會講解閱讀方法中涉及的一些變量的意義。瞭解HashMap的特色、適用場景。php

若是本文中有不正確的結論、說法,請你們提出和我討論,共同進步,謝謝。java

2 概要

歸納的說,HashMap 是一個關聯數組、哈希表,它是線程不安全的,容許key爲null,value爲null。遍歷時無序。 
其底層數據結構是數組稱之爲哈希桶,每一個桶裏面放的是鏈表,鏈表中的每一個節點,就是哈希表中的每一個元素。 
在JDK8中,當鏈表長度達到8,會轉化成紅黑樹,以提高它的查詢、插入效率,它實現了Map<K,V>, Cloneable, Serializable接口。node

因其底層哈希桶的數據結構是數組,因此也會涉及到擴容的問題。sql

HashMap的容量達到threshold域值時,就會觸發擴容。擴容先後,哈希桶的長度必定會是2的次方。 
這樣在根據key的hash值尋找對應的哈希桶時,能夠用位運算替代取餘操做,更加高效。數組

而key的hash值,並不只僅只是key對象的hashCode()方法的返回值,還會通過擾動函數的擾動,以使hash值更加均衡。 
由於hashCode()int類型,取值範圍是40多億,只要哈希函數映射的比較均勻鬆散,碰撞概率是很小的。 
但就算本來的hashCode()取得很好,每一個key的hashCode()不一樣,可是因爲HashMap的哈希桶的長度遠比hash取值範圍小,默認是16,因此當對hash值以桶的長度取餘,以找到存放該key的桶的下標時,因爲取餘是經過與操做完成的,會忽略hash值的高位。所以只有hashCode()的低位參加運算,發生不一樣的hash值,可是獲得的index相同的狀況的概率會大大增長,這種狀況稱之爲hash碰撞。 即,碰撞率會增大。緩存

擾動函數就是爲了解決hash碰撞的。它會綜合hash值高位和低位的特徵,並存放在低位,所以在與運算時,至關於高低位一塊兒參與了運算,以減小hash碰撞的機率。(在JDK8以前,擾動函數會擾動四次,JDK8簡化了這個操做)安全

擴容操做時,會new一個新的Node數組做爲哈希桶,而後將原哈希表中的全部數據(Node節點)移動到新的哈希桶中,至關於對原哈希表中全部的數據從新作了一個put操做。因此性能消耗很大,可想而知,在哈希表的容量越大時,性能消耗越明顯。markdown

擴容時,若是發生過哈希碰撞,節點數小於8個。則要根據鏈表上每一個節點的哈希值,依次放入新哈希桶對應下標位置。 
由於擴容是容量翻倍,因此原鏈表上的每一個節點,如今可能存放在原來的下標,即low位, 或者擴容後的下標,即high位。 high位= low位+原哈希桶容量 
若是追加節點後,鏈表數量》=8,則轉化爲紅黑樹網絡

由迭代器的實現能夠看出,遍歷HashMap時,順序是按照哈希桶從低到高,鏈表從前日後,依次遍歷的。屬於無序集合。數據結構

整個HashMap示意圖:圖片來源於網絡,侵刪:

HashMap的源碼中,充斥個各類位運算代替常規運算的地方,以提高效率: 
* 與運算替代模運算。用 hash & (table.length-1) 替代 hash % (table.length) 
* 用if ((e.hash & oldCap) == 0)判斷擴容後,節點e處於低區仍是高區。

3 鏈表節點Node

在開始以前,咱們先看一下掛載在哈希表上的元素,鏈表的結構:

static class Node<K,V> implements Map.Entry<K,V> { final int hash;//哈希值 final K key;//key V value;//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; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } //每個節點的hash值,是將key的hashCode 和 value的hashCode 亦或獲得的。 public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } //設置新的value 同時返回舊value public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } } 

 

**由此可知,這是一個單鏈表~。 
每個節點的hash值,是將key的hashCode 和 value的hashCode 亦或獲得的。**

4 構造函數

//最大容量 2的30次方 static final int MAXIMUM_CAPACITY = 1 << 30; //默認的加載因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; //哈希桶,存放鏈表。 長度是2的N次方,或者初始化時爲0. transient Node<K,V>[] table; //加載因子,用於計算哈希表元素數量的閾值。 threshold = 哈希桶.length * loadFactor; final float loadFactor; //哈希表內元素數量的閾值,當哈希表內元素數量超過閾值時,會發生擴容resize()。 int threshold; public HashMap() { //默認構造函數,賦值加載因子爲默認的0.75f this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } public HashMap(int initialCapacity) { //指定初始化容量的構造函數 this(initialCapacity, DEFAULT_LOAD_FACTOR); } //同時指定初始化容量 以及 加載因子, 用的不多,通常不會修改loadFactor public HashMap(int initialCapacity, float loadFactor) { //邊界處理 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); //初始容量最大不能超過2的30次方 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; //顯然加載因子不能爲負數 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; //設置閾值爲 》=初始化容量的 2的n次方的值 this.threshold = tableSizeFor(initialCapacity); } //新建一個哈希表,同時將另外一個map m 裏的全部元素加入表中 public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); } 
//根據指望容量cap,返回2的n次方形式的 哈希桶的實際容量 length。 返回值通常會>=cap static final int tableSizeFor(int cap) { //通過下面的 或 和位移 運算, n最終各位都是1。 int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; //判斷n是否越界,返回 2的n次方做爲 table(哈希桶)的閾值 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }

 

//將另外一個Map的全部元素加入表中,參數evict初始化時爲false,其餘狀況爲true final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { //拿到m的元素數量 int s = m.size(); //若是數量大於0 if (s > 0) { //若是當前表是空的 if (table == null) { // pre-size //根據m的元素數量和當前表的加載因子,計算出閾值 float ft = ((float)s / loadFactor) + 1.0F; //修正閾值的邊界 不能超過MAXIMUM_CAPACITY int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); //若是新的閾值大於當前閾值 if (t > threshold) //返回一個 》=新的閾值的 知足2的n次方的閾值 threshold = tableSizeFor(t); } //若是當前元素表不是空的,可是 m的元素數量大於閾值,說明必定要擴容。 else if (s > threshold) resize(); //遍歷 m 依次將元素加入當前表中。 for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } }

 

先看一下擴容函數: 這是一個重點!重點!重點! 
**初始化或加倍哈希桶大小。若是是當前哈希桶是null,分配符合當前閾值的初始容量目標。 
不然,由於咱們擴容成之前的兩倍。 
在擴容時,要注意區分之前在哈希桶相同index的節點,如今是在之前的index裏,仍是index+oldlength 裏**

final Node<K,V>[] resize() { //oldTab 爲當前表的哈希桶 Node<K,V>[] oldTab = table; //當前哈希桶的容量 length int oldCap = (oldTab == null) ? 0 : oldTab.length; //當前的閾值 int oldThr = threshold; //初始化新的容量和閾值爲0 int newCap, newThr = 0; //若是當前容量大於0 if (oldCap > 0) { //若是當前容量已經到達上限 if (oldCap >= MAXIMUM_CAPACITY) { //則設置閾值是2的31次方-1 threshold = Integer.MAX_VALUE; //同時返回當前的哈希桶,再也不擴容 return oldTab; }//不然新的容量爲舊的容量的兩倍。 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)//若是舊的容量大於等於默認初始容量16 //那麼新的閾值也等於舊的閾值的兩倍 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;//此時新表的容量爲默認的容量 16 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//新的閾值爲默認容量16 * 默認加載因子0.75f = 12 } if (newThr == 0) {//若是新的閾值是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) { //取出當前的節點 e Node<K,V> e; //若是當前桶中有元素,則將鏈表賦值給e if ((e = oldTab[j]) != null) { //將原哈希桶置空以便GC oldTab[j] = null; //若是當前鏈表中就一個元素,(沒有發生哈希碰撞) if (e.next == null) //直接將這個元素放置在新的哈希桶裏。 //注意這裏取下標 是用 哈希值 與 桶的長度-1 。 因爲桶的長度是2的n次方,這麼作實際上是等於 一個模運算。可是效率更高 newTab[e.hash & (newCap - 1)] = e; //若是發生過哈希碰撞 ,並且是節點數超過8個,轉化成了紅黑樹(暫且不談 避免過於複雜, 後續專門研究一下紅黑樹) else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //若是發生過哈希碰撞,節點數小於8個。則要根據鏈表上每一個節點的哈希值,依次放入新哈希桶對應下標位置。 else { // preserve order //由於擴容是容量翻倍,因此原鏈表上的每一個節點,如今可能存放在原來的下標,即low位, 或者擴容後的下標,即high位。 high位= low位+原哈希桶容量 //低位鏈表的頭結點、尾節點 Node<K,V> loHead = null, loTail = null; //高位鏈表的頭節點、尾節點 Node<K,V> hiHead = null, hiTail = null; Node<K,V> next;//臨時節點 存放e的下一個節點 do { next = e.next; //這裏又是一個利用位運算 代替常規運算的高效點: 利用哈希值 與 舊的容量,能夠獲得哈希值去模後,是大於等於oldCap仍是小於oldCap,等於0表明小於oldCap,應該存放在低位,不然存放在高位 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); //將低位鏈表存放在原index處, if (loTail != null) { loTail.next = null; newTab[j] = loHead; } //將高位鏈表存放在新index處 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }

 

再看一下 往哈希表裏插入一個節點的putVal函數,若是參數onlyIfAbsent是true,那麼不會覆蓋相同key的值value。若是evict是false。那麼表示是在初始化時調用的

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //tab存放 當前的哈希桶, p用做臨時鏈表節點 Node<K,V>[] tab; Node<K,V> p; int n, i; //若是當前哈希表是空的,表明是初始化 if ((tab = table) == null || (n = tab.length) == 0) //那麼直接去擴容哈希表,而且將擴容後的哈希桶長度賦值給n n = (tab = resize()).length; //若是當前index的節點是空的,表示沒有發生哈希碰撞。 直接構建一個新節點Node,掛載在index處便可。 //這裏再囉嗦一下,index 是利用 哈希值 & 哈希桶的長度-1,替代模運算 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else {//不然 發生了哈希衝突。 //e Node<K,V> e; K k; //若是哈希值相等,key也相等,則是覆蓋value操做 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;//將當前節點引用賦值給e 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); //若是追加節點後,鏈表數量》=8,則轉化爲紅黑樹 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; } } //若是e不是null,說明有須要覆蓋的節點, if (e != null) { // existing mapping for key //則覆蓋節點值,並返回原oldValue V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; //這是一個空實現的函數,用做LinkedHashMap重寫使用。 afterNodeAccess(e); return oldValue; } } //若是執行到了這裏,說明插入了一個新的節點,因此會修改modCount,以及返回null。 //修改modCount ++modCount; //更新size,並判斷是否須要擴容。 if (++size > threshold) resize(); //這是一個空實現的函數,用做LinkedHashMap重寫使用。 afterNodeInsertion(evict); return null; }
  • wNode以下:構建一個鏈表節點
// Create a regular (non-tree) node Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) { return new Node<>(hash, key, value, next); }
// Callbacks to allow LinkedHashMap post-actions void afterNodeAccess(Node<K,V> p) { } void afterNodeInsertion(boolean evict) { }

小結: 

* 運算儘可能都用位運算代替,更高效。 
* 對於擴容致使須要新建數組存放更多元素時,除了要將老數組中的元素遷移過來,也記得將老數組中的引用置null,以便GC 
* 取下標 是用 哈希值 與運算 (桶的長度-1) i = (n - 1) & hash。 因爲桶的長度是2的n次方,這麼作實際上是等於 一個模運算。可是效率更高 
* 擴容時,若是發生過哈希碰撞,節點數小於8個。則要根據鏈表上每一個節點的哈希值,依次放入新哈希桶對應下標位置。 
* 由於擴容是容量翻倍,因此原鏈表上的每一個節點,如今可能存放在原來的下標,即low位, 或者擴容後的下標,即high位。 high位= low位+原哈希桶容量 
* 利用哈希值 與運算 舊的容量 ,if ((e.hash & oldCap) == 0),能夠獲得哈希值去模後,是大於等於oldCap仍是小於oldCap,等於0表明小於oldCap,應該存放在低位,不然存放在高位。這裏又是一個利用位運算 代替常規運算的高效點 
* 若是追加節點後,鏈表數量》=8,則轉化爲紅黑樹 
* 插入節點操做時,有一些空實現的函數,用做LinkedHashMap重寫使用。

5 增、改

1往表中插入或覆蓋一個key-value

public V put(K key, V value) { //先根據key,取得hash值。 再調用上一節的方法插入節點 return putVal(hash(key), key, value, false, true); }

這個根據key取hash值的函數也要關注一下,它稱之爲「擾動函數」,關於這個函數的用處 開頭已經總結過了:

static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }

 

而key的hash值,並不只僅只是key對象的hashCode()方法的返回值,還會通過擾動函數的擾動,以使hash值更加均衡。 
由於hashCode()int類型,取值範圍是40多億,只要哈希函數映射的比較均勻鬆散,碰撞概率是很小的。 
但就算本來的hashCode()取得很好,每一個key的hashCode()不一樣,可是因爲HashMap的哈希桶的長度遠比hash取值範圍小,默認是16,因此當對hash值以桶的長度取餘,以找到存放該key的桶的下標時,因爲取餘是經過與操做完成的,會忽略hash值的高位。所以只有hashCode()的低位參加運算,發生不一樣的hash值,可是獲得的index相同的狀況的概率會大大增長,這種狀況稱之爲hash碰撞。 即,碰撞率會增大。

擾動函數就是爲了解決hash碰撞的。它會綜合hash值高位和低位的特徵,並存放在低位,所以在與運算時,至關於高低位一塊兒參與了運算,以減小hash碰撞的機率。(在JDK8以前,擾動函數會擾動四次,JDK8簡化了這個操做)

2往表中批量增長數據

public void putAll(Map<? extends K, ? extends V> m) { //這個函數上一節也已經分析過。//將另外一個Map的全部元素加入表中,參數evict初始化時爲false,其餘狀況爲true putMapEntries(m, true); }

3 只會往表中插入 key-value, 若key對應的value以前存在,不會覆蓋。(jdk8增長的方法)

@Override
    public V putIfAbsent(K key, V value) { return putVal(hash(key), key, value, true, true); }

6 刪

以key爲條件刪除

若是key對應的value存在,則刪除這個鍵值對。 並返回value。若是不存在 返回null。

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

//從哈希表中刪除某個節點, 若是參數matchValue是true,則必須key 、value都相等才刪除。 
//若是movable參數是false,在刪除節點時,不移動其餘節點

final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { // p 是待刪除節點的前置節點 Node<K,V>[] tab; Node<K,V> p; int n, index; //若是哈希表不爲空,則根據hash值算出的index下 有節點的話。 if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { //node是待刪除節點 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;//將待刪除節點引用賦給node else if ((e = p.next) != null) {//不然循環遍歷 找到待刪除節點,賦值給node 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); } } //若是有待刪除節點node, 且 matchValue爲false,或者值也相等 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)//若是node == p,說明是鏈表頭是待刪除節點 tab[index] = node.next; else//不然待刪除節點在表中間 p.next = node.next; ++modCount;//修改modCount --size;//修改size afterNodeRemoval(node);//LinkedHashMap回調函數 return node; } } return null; }

 

void afterNodeRemoval(Node<K,V> p) { }

 

以key value 爲條件刪除

@Override
    public boolean remove(Object key, Object value) { //這裏傳入了value 同時matchValue爲true return removeNode(hash(key), key, value, true, true) != null; }

 

7 查

以key爲條件,找到返回value。沒找到返回null

public V get(Object key) { Node<K,V> e; //傳入擾動後的哈希值 和 key 找到目標節點Node return (e = getNode(hash(key), key)) == null ? null : e.value; }

 

//傳入擾動後的哈希值 和 key 找到目標節點Node final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //查找過程和刪除基本差很少, 找到返回節點,不然返回null 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; }

判斷是否包含該key

public boolean containsKey(Object key) { return getNode(hash(key), key) != null; }

 

判斷是否包含value

public boolean containsValue(Object value) { Node<K,V>[] tab; V v; //遍歷哈希桶上的每個鏈表 if ((tab = table) != null && size > 0) { for (int i = 0; i < tab.length; ++i) { for (Node<K,V> e = tab[i]; e != null; e = e.next) { //若是找到value一致的返回true if ((v = e.value) == value || (value != null && value.equals(v))) return true; } } } return false; }

java8新增,帶默認值的get方法

以key爲條件,找到了返回value。不然返回defaultValue

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

 

遍歷

//緩存 entrySet transient Set<Map.Entry<K,V>> entrySet; */ public Set<Map.Entry<K,V>> entrySet() { Set<Map.Entry<K,V>> es; return (es = entrySet) == null ? (entrySet = new EntrySet()) : es; }

 

final class EntrySet extends AbstractSet<Map.Entry<K,V>> { public final int size() { return size; } public final void clear() { HashMap.this.clear(); } //通常咱們用到EntrySet,都是爲了獲取iterator public final Iterator<Map.Entry<K,V>> iterator() { return new EntryIterator(); } //最終仍是調用getNode方法 public final boolean contains(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry<?,?> e = (Map.Entry<?,?>) o; Object key = e.getKey(); Node<K,V> candidate = getNode(hash(key), key); return candidate != null && candidate.equals(e); } //最終仍是調用removeNode方法 public final boolean remove(Object o) { if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>) o; Object key = e.getKey(); Object value = e.getValue(); return removeNode(hash(key), key, value, true, true) != null; } return false; } //。。。 }

 

//EntryIterator的實現:

final class EntryIterator extends HashIterator implements Iterator<Map.Entry<K,V>> { public final Map.Entry<K,V> next() { return nextNode(); } }

 

abstract class HashIterator { Node<K,V> next; // next entry to return Node<K,V> current; // current entry int expectedModCount; // for fast-fail int index; // current slot HashIterator() { //由於hashmap也是線程不安全的,因此要保存modCount。用於fail-fast策略 expectedModCount = modCount; Node<K,V>[] t = table; current = next = null; index = 0; //next 初始時,指向 哈希桶上第一個不爲null的鏈表頭 if (t != null && size > 0) { // advance to first entry do {} while (index < t.length && (next = t[index++]) == null); } } public final boolean hasNext() { return next != null; } //由這個方法能夠看出,遍歷HashMap時,順序是按照哈希桶從低到高,鏈表從前日後,依次遍歷的。屬於無序集合。 final Node<K,V> nextNode() { Node<K,V>[] t; Node<K,V> e = next; //fail-fast策略 if (modCount != expectedModCount) throw new ConcurrentModificationException(); if (e == null) throw new NoSuchElementException(); //依次取鏈表下一個節點, if ((next = (current = e).next) == null && (t = table) != null) { //若是當前鏈表節點遍歷完了,則取哈希桶下一個不爲null的鏈表頭 do {} while (index < t.length && (next = t[index++]) == null); } return e; } public final void remove() { Node<K,V> p = current; if (p == null) throw new IllegalStateException(); ////fail-fast策略 if (modCount != expectedModCount) throw new ConcurrentModificationException(); current = null; K key = p.key; //最終仍是利用removeNode 刪除節點 removeNode(hash(key), key, null, false, false); expectedModCount = modCount; } }

 

8 總結

HashMap特色和精髓能夠參看本文第二章【概要】 和第四章的【小結】部分。

後續會另開新篇聊一聊紅黑樹。

20170920 add,從網上轉了一張圖,聽說來自美團,侵刪: 
20170920,從網上轉了一張圖,聽說來自美團,侵刪

9 與HashTable的區別

  • 與之相比HashTable是線程安全的,且不容許key、value是null。
  • HashTable默認容量是11。
  • HashTable是直接使用key的hashCode(key.hashCode())做爲hash值,不像HashMap內部使用static final int hash(Object key)擾動函數對key的hashCode進行擾動後做爲hash值。
  • HashTable取哈希桶下標是直接用模運算%.(由於其默認容量也不是2的n次方。因此也沒法用位運算替代模運算)
  • 擴容時,新容量是原來的2倍+1。int newCapacity = (oldCapacity << 1) + 1;
  • HashtableDictionary的子類同時也實現了Map接口,HashMapMap接口的一個實現類;
相關文章
相關標籤/搜索