想看我更多文章:【張旭童的部落格】blog.csdn.net/zxt0601
想來gayhub和我gaygayup:【mcxtzhang的Github主頁】github.com/mcxtzhang
1 概述
本文將從幾個常用方法下手,來閱讀HashMap
的原始碼。
按照從構造方法->常用API(增、刪、改、查)的順序來閱讀原始碼,並會講解閱讀方法中涉及的一些變數的意義。瞭解HashMap
的特點、適用場景。
如果本文中有不正確的結論、說法,請大家提出和我討論,共同進步,謝謝。
2 概要
概括的說,HashMap
是一個關聯陣列、雜湊表,它是執行緒不安全的,允許key為null,value為null。遍歷時無序。
其底層資料結構是陣列稱之為雜湊桶,每個桶裡面放的是連結串列,連結串列中的每個節點,就是雜湊表中的每個元素。
在JDK8中,當連結串列長度達到8,會轉化成紅黑樹,以提升它的查詢、插入效率,它實現了Map<K,V>, Cloneable, Serializable
介面。
因其底層雜湊桶的資料結構是陣列,所以也會涉及到擴容的問題。
當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操作。所以效能消耗很大,可想而知,在雜湊表的容量越大時,效能消耗越明顯。
擴容時,如果發生過雜湊碰撞,節點數小於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;
}複製程式碼
newNode
如下:構建一個連結串列節點
// 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,從網上轉了一張圖,據說來自美團,侵刪:
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;
Hashtable
是Dictionary
的子類同時也實現了Map
介面,HashMap
是Map
介面的一個實現類;