面試必備1:HashMap(JDK1.8)增、刪、改、查以及擴容源碼分析
首先附上我的幾篇其它文章鏈接感興趣的可以看看,如果文章有異議的地方歡迎指出,共同進步,順便點贊謝謝!!!
Android framework 源碼分析之Activity啓動流程(android 8.0)
Android studio編寫第一個NDK工程的過程詳解(附Demo下載地址)
面試必備2:JDK1.8LinkedHashMap實現原理及源碼分析
Android事件分發機制原理及源碼分析
View事件的滑動衝突以及解決方案
Handler機制一篇文章深入分析Handler、Message、MessageQueue、Looper流程和源碼
Android三級緩存原理及用LruCache、DiskLruCache實現一個三級緩存的ImageLoader
HashMap概述:
在分析HashMap的源碼前,需要了解一下幾個問題。
面試必備2LinkedHashMap的源碼分析(基於JDK1.8)
1:HashMap的數據結構
我們需要先知道HashMap是基於什麼樣的數據結構來進行數據存儲的,知道了這些我們再去看源碼就容易的多。
散列表(哈希表) 我們常用的數據結構就是數組和鏈表,數組具有增刪慢查找快的特點,而鏈表具有增刪快查找慢 的特點;基於上述特點,HashMap 即想要查詢效率快,又想增刪效率高,基於這樣的特點HashMap的數據結構就是通過數組和鏈表組成的散列鏈表。
一直到JDK7爲止,HashMap的結構都基於一個數組以及多個鏈表的實現,hash值衝突的時候,就將對應節點以鏈表的形式存儲;JDK8中,HashMap採用的是數組(位桶)+鏈表/紅黑樹組成,當同一個hash值的節點數不小於8時,將不再以單鏈表的形式存儲了,會被調整成一顆紅黑樹,提高查詢效率,這就是JDK7與JDK8中HashMap實現的最大區別。
2:存儲節點 : Node 和TreeNode
HashMap存儲數據時如何組織出這樣一個散列表呢?HashMap中將我們存入的每一個數據封裝成一個Node類(Node節點),Node應該有以下下屬性 :key--------鍵、value-----值 組成我們put的鍵值對,既然要組織起鏈表則必須有Node next屬性、還有一個根據key算出的int 類型hash值,hash值用來確定該該節點所在數組的索引後面會進行詳細描述。
/**
* 散列表的節點 Node 部分源碼
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//用於確定該節點所在數組的位置, 下標
final K key;// 我們所操作的鍵
V value;// 值
Node<K,V> next;//存儲下一個節點,通過next組織起鏈表
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;//位置
this.key = key;
this.value = value;
this.next = next;
}
/**
* 返回當前節點的根節點
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
}
當鏈表長度大於8時將鏈表轉換成紅黑樹,紅黑樹節點TreeNode部分源碼
static final class TreeNode<K,V> extends LinkedHashMap.LinkedHashMapEntry<K,V> {
TreeNode<K,V> parent; // 父節點
TreeNode<K,V> left;//左子樹
TreeNode<K,V> right;//右子樹
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;//顏色屬性
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
當我們放入或取出元素時大致流程是:
- 現根據hash函數去求出key的Hash值,至於求hash值得算法將在後面進行詳細分析
- 根據上一步的hash值可以確定在數組的哪個位置,定下標(數組的索引)
- 根據索引從數組中獲取數據,如果爲NULL 則將該元素直接放入或取出(直接在數組中操作),如果該位置數據不爲NuLL,則需要向該元素所在的鏈表進行數據的操作。
哈希衝突(哈希碰撞)
如果兩個不同的元素,通過哈希函數得出的實際存儲地址相同怎麼辦?也就是說,當我們對某個元素進行哈希運算,得到一個存儲地址,然後要進行插入的時候,發現已經被其他元素佔用了,其實這就是所謂的哈希衝突,也叫哈希碰撞。好的哈希函數會盡可能地保證 計算簡單和散列地址分佈均勻,但是,我們需要清楚的是,數組是一塊連續的固定長度的內存空間,再好的哈希函數也不能保證得到的存儲地址絕對不發生衝突。哈希衝突的解決方案有多種:開放定址法(發生衝突,繼續尋找下一塊未被佔用的存儲地址),再散列函數法,鏈地址法,而HashMap即是採用了鏈地址法,也就是數組+鏈表的方式。
3:HashMap中散列表中數組(或位桶)如何表示?
HashMap源碼中與數組相關的幾個成員屬性:
- 數組的表示:
transient Node<K,V>[] table;
- 數組的大小 : 數組的初始化需要一個指定的大小,在HashMap中 數組的大小永遠是 2的冪次方(原因將在源碼中分析)
- 數組的默認大小:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 , 1 << 4 二進制中1左移四位就是2^4=16, 不直接寫16的原因是,效率高,計算機最終讀取的二進制
; - 數組的最大容量:
static final int MAXIMUM_CAPACITY = 1 << 30//最大爲2^30,使用位移操作符的原因也是效率高 ;
- 數組擴容因子:
static final float DEFAULT_LOAD_FACTOR = 0.75f//用於確定數組擴容的臨界值;
- 以存放數據大小:
transient int size//記錄 hashMap 當前存儲的元素的數量;
- 數組擴容的臨界值:
int threshold;//數組已使用量size>threshold時需要擴容,threshold=數組大小*擴容因子提升效率,沒有必要等到數組都用完了在進行擴容,每次擴大2倍
**注意:數組的擴容是在當已使用量大於數組容量*擴容因子(默認0.75)**時進行擴容
4:HashMap中散列表中鏈表的長度限制:長度越長存儲查詢效率低的問題
鏈表越長,期查詢效率越低,所以鏈表的長度也有一個限制,我們稱爲閾值 ,JDK1.8的時候,鏈表的長度大於閾值,將其結構改成一個紅黑二叉樹,以提升差值效率,同時也伴隨者性能的損耗增加
HashMap源碼中與鏈表相關的幾個成員屬性:
- 轉換紅黑樹的閾值:
static final int TREEIFY_THRESHOLD = 8;//閾值 鏈表越深查找存儲效率都低 ,超過8以後將其轉換成紅黑二叉樹 ,提升查找效率
- 轉換成鏈表的閾值
static final int UNTREEIFY_THRESHOLD = 6; //當某個桶節點數量小於6時,會轉換爲鏈表,前提是它當前是紅黑樹結構。
static final int MIN_TREEIFY_CAPACITY = 64;//當整個hashMap中元素數量大於64時,也會進行轉爲紅黑樹結構。
6:新的數據封裝成的Node對象如何去存儲:
根據key計算hashCode ----》int類型的值,hash(key)然後就知道要存在數組的那個位置
5:HashMap中的屬性的概述
這裏將對HashMap屬性進行描述,以便於理解源碼
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
//序列號,序列化的時候使用。
private static final long serialVersionUID = 362498820763181265L;
/**默認容量,1向左移位4個,00000001變成00010000,也就是2的4次方爲16,使用移位是因爲移位是計算機基礎運算,效率比加減乘除快。**/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量,2的30次方。
static final int MAXIMUM_CAPACITY = 1 << 30;
//加載因子,用於擴容使用。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//當某個桶節點數量大於8時,會轉換爲紅黑樹。
static final int TREEIFY_THRESHOLD = 8;
//當某個桶節點數量小於6時,會轉換爲鏈表,前提是它當前是紅黑樹結構。
static final int UNTREEIFY_THRESHOLD = 6;
//當整個hashMap中元素數量大於64時,也會進行轉爲紅黑樹結構。
static final int MIN_TREEIFY_CAPACITY = 64;
//存儲元素的數組,transient關鍵字表示該屬性不能被序列化
transient Node<K,V>[] table;
//將數據轉換成set的另一種存儲形式,這個變量主要用於迭代功能。
transient Set<Map.Entry<K,V>> entrySet;
//元素數量
transient int size;
//統計該map修改的次數
transient int modCount;
//臨界值,也就是元素數量達到臨界值時,會進行擴容。
int threshold;
//也是加載因子,只不過這個是變量。
final float loadFactor;
....
}
HashMap源碼分析:
瞭解了概述中的幾個概念後,將從對HashMap的put、get、remove三個方法進行源碼分析。
1:HashMap的put(key,value)方法源碼
put方法的大致流程:
- 根據傳入的key,計算hash值
- 判斷鍵值對數組tab[]是否爲空或爲null,否則以默認大小resize()進行數組的初始化操作;
- 根據鍵值key計算hash值得到插入的數組索引i,如果tab[i]==null,直接新建節點添加,否則轉入3
- 判斷當前數組中處理hash衝突的方式爲鏈表還是紅黑樹(check第一個節點類型即可),分別處理
//返回值扔然爲Value
public V put(K key, V value) {
//1:hash(key)先找存在哪裏,先算key的hash值
//2: putVal(hash(key), key, value, false, true)方法進行存儲
return putVal(hash(key), key, value, false, true);
}
//根據key求出Hash值,通過這去求散列表的下標
static final int hash(Object key) {
int h;//32位數,不足高位補0
// (h = key.hashCode()) ^ (h >>> 16)
// 1: key的HashCode h
//2: h先做了個位移運算,向右邊位移16位即h >>> 16
/// 3:然後將兩個值進行異或預算 充分利用hash的每一位數,將h的高16位異或h的低16位得到一個值, 使得高位也可以參與hash,更大程度上減少了碰撞率。
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
putVal方法源碼:
//,如果參數onlyIfAbsent是true,那麼不會覆蓋相同key的值value。如果evict是false。那麼表示是在初始化時調用的
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)//數組table爲null
n = (tab = resize()).length;//resize() 作用1:初始化數組 n記錄數組的大小
if ((p = tab[i = (n - 1) & hash]) == null) // tab[i = (n - 1) & hash]==null, 即數組tab[i]==null,沒有元素直接新建節點添加
/**
* 根據hash和數組長度算出數組下標i table[i]取出對應元素
* i = (n - 1) & hash 算出數組下標的優點?
* 1:防止數組越界:這麼寫是爲了解決hash有可能越界問題 ,即hash不能大於n 數組長度,
* 數組的大小n一定是2幾次冪,只有這樣n-1,n-1二進制表示時每位的值都爲1,從而保證在(n-1)&hash值的值永遠<=n-1 ,保證不會數組越界
* 例如:當n=16默認值時,n - 1=15 的二進制 01111 和前面的hash值進行與運算 的值永遠小於等於01111(15),不會越界
* 2:提高效率:(n - 1) & hash等價於 模/n取餘 這樣寫是因爲與運算比模運算效率高
* 3: 數組的大小n一定是2幾次冪,n-1 的二進制形式每位都是1,在&hash時導致數組的散列性變大,降低了hash碰撞的概率
*/
tab[i] = newNode(hash, key, value, null);
else {//tab[i]!=null需要向鏈表或紅黑樹中存放
Node<K,V> e; K k;
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
// 如果這個元素的key與要插入的一樣,那麼就替換
e = p;
else if (p instanceof TreeNode)
//1.如果當前節點是TreeNode類型的數據,執行putTreeVal方法, 向紅黑樹中存放
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//遍歷這條鏈子上的數據,跟jdk7沒什麼區別
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {//即table[i].next==null,table[i]就是尾節點
//向鏈表尾部添加數據
p.next = newNode(hash, key, value, null);
//項鍊表中添加完元素後判斷,鏈表長度是否超過8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//長度超過8,執行treeifyBin方法,將鏈表轉換成紅黑樹
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 表示在桶中找到key值、hash值與插入元素相等的結點
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);//這個爲子類HashMap服務,進行lru排序,在這裏無需研究
return null;
}
treeifyBin方法源碼:將鏈表轉換成紅黑二叉樹
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);//將Node轉成TreeNode
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);
}
}
2:HashMap的擴容機制:resize()源碼
resize的作用:1:初始化數組table 2: 擴容
構造hash表時,如果不指明初始大小,默認大小爲16(即Node數組大小16),如果Node[]數組中的元素達到(填充比*Node.length)重新調整HashMap大小 變爲原來2倍大小,擴容很耗時
- 每次擴展的時候,都是擴展2倍;
- 擴展後Node對象的位置要麼在原位置,要麼移動到原偏移量兩倍的位置。
//resize 作用1:初始化數組 2擴容
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) {//數組大於0進行擴容操作
if (oldCap >= MAXIMUM_CAPACITY) {//如果容量已大於最大值
threshold = Integer.MAX_VALUE;//修改擴容臨界值
return oldTab;
}
//如果oldCap << 1擴大兩倍,滿足擴容的條件
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 左移1位擴大兩倍容量
}
// 如果舊錶的長度的是0,就是說第一次初始化表
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;//threshold 容量*負載因子 =臨界值
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//初始化數組 一開始默認到校
table = newTab;//把新表賦值給table
if (oldTab != null) {//原表不是空要把原表中數據移動到新表中
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)//說明這個node沒有鏈表直接放在新表的數組e.hash & (newCap - 1)位置
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//如果是紅黑樹則,轉爲鏈表
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//如果e後邊有鏈表,到這裏表示e後面帶着個單鏈表,需要遍歷單鏈表,將每個結點重新計算在新表的位置,並進行搬運
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),可以得到哈希值去模後,
//是大於等於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);
if (loTail != null) {//lo隊不爲null,放在新表原位置
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {//hi隊不爲null,放在新表j+oldCap位置
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;//返回初始化或者擴容後的數組
}
3:小結:
- 運算儘量都用位運算代替,更高效。
- 對於擴容導致需要新建數組存放更多元素時,除了要將老數組中的元素遷移過來,也記得將老數組中的引用置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重寫使用。
4:HashMap的get(key)方法源碼分析:
下面簡單說下 get(key) 的過程:
get(key)方法時獲取key的hash值,計算hash&(n-1)得到在鏈表數組中的位置first=tab[hash&(n-1)],先判斷first的key是否與參數key相等,不等就遍歷後面的鏈表找到相同的key值返回對應的Value值即可,即先根據hash值確定數組(哈希桶)位置,然後根據key相等取值 , hash相等&&key相等的節點
//傳入鍵 key
public V get(Object key) {
Node<K,V> e;
//根據hash函數得到的hash值和key去 getNode(hash(key), key) 返回null或者Node.value
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
getNode(hash, key)方法源碼分析:
//根據 hash 和key獲取node節點
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;//在tab數組中經過散列的第一個位置
// table已經初始化,長度大於0,根據hash尋找table中的項也不爲空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash &&// 判斷是不是第一個節點,是的話從數組中返回
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//否則
if ((e = first.next) != null) {
// 取出first的下一個節點,如果爲爲紅黑樹結點
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;
}
5:remove方法源碼分析
public V remove(Object key) {
Node<K,V> e;
//這裏傳入了value 同時matchValue爲true
return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value;
}
removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable)源碼
/**
* Implements Map.remove and related methods
* @param hash 哈希值
* @param key the key
* @param value the value to match if matchValue, else ignored
* @param matchValue 爲true的話,則表示刪除它key對應的value,不刪除key
* @param movable 如果爲false,則表示刪除後,不移動節點,爲true則移動節點
* @return the node, or null if none
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
//tab 哈希數組,p 數組下標的節點,n 長度,index 當前數組下標
Node<K,V>[] tab; Node<K,V> p; int n, index;
//哈希數組不爲null,且長度大於0,然後獲得到要刪除key的節點所在是數組下標位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
//node 存儲要刪除的節點,e 臨時變量,k 當前節點的key,v 當前節點的value
Node<K,V> node = null, e; K k; V v;
//如果數組下標的節點正好是要刪除的節點,把值賦給臨時變量node
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不再是數組下標的節點,而是要刪除結點的上一個結點**/
p = e;
} while ((e = e.next) != null);
}
}
//找到要刪除的節點後,判斷!matchValue,我們正常的remove刪除,!matchValue都爲true
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;
/**此方法在hashMap中是爲了讓子類去實現,主要是對刪除結點後的鏈表關係進行處理**/
afterNodeRemoval(node);
//返回刪除的節點
return node;
}
}
//返回null則表示沒有該節點,刪除失敗
return null;
}
HashMap的遍歷entrySet()
entrySet()源碼:
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
//直接返回成員變量entrySet==null時 new EntrySet()並返回,否則直接返回
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
集合的遍歷主要是通過內部類EntrySet
的iterator()
方法實現,EntrySet的部分源碼如下:
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<Map.Entry<K,V>> iterator() {
//返回EntryIterator類型的迭代器
return new EntryIterator();
}
....省略部分源碼...
}
iterator方法返回EntryIterator類型的迭代器其源碼如下
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
//Iterator的`next`方法就是調用父類`HashIterator`的`nextNode`方法獲取下一個節點
public final Map.Entry<K,V> next() { return nextNode(); }
}
nextNode方法是父類HashIterator
提供的方法,即HaapMap的迭代的核心就是HashIterator
源碼的源碼如下,這裏是重點!!!
abstract class HashIterator {
Node<K,V> next; // 下一個節點
Node<K,V> current; // 當前節點
int expectedModCount; // HashMap是線程不安全的,在迭代時通過expectedModCount去記錄成員變量modCount,判斷迭代過程中的併發修改異常
int index; // 當前數組的索引
/**
* 構造器中對屬性進行了初始化,
* 並通過while循環得到數組中的第一個不爲空的元素下標以及值,並將此元素值賦給next
*/
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
//while循環得到數組中的第一個不爲空的元素下標以及值,並將此元素值賦給next
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;
}
/**
* nextNode()的方法,就是Iterator中next方法中調用的,即Iterator中next方法就是nextNode方法
* 其遍歷HashMap過程就是,就是依次遍歷數組中的鏈表,取出下一個節點
*/
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;//e,爲當前要返回的節點
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
//HashMap遍歷的核心思想
//e爲當前要返回的節點,
//獲取e的下一節點,並賦值給next,如果此時的next爲空,則說明當前鏈表已經遍歷完畢,
//那麼進入判斷體,開始通過while遍歷下一個數組桶中的鏈表,並將此鏈表的頭結點賦值給next;
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
/**
* Iterator的remove刪除方法 本質上還是調用了HashMap的removeNode方法
* 只是在調用之前,通過modCount != expectedModCount時拋出併發修改異常,處理線程不安全問題,
* 如果相等則調用HashMap的removeNode方法移除節點
*
*/
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)//處理多線程併發問題
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
HashMap的遍歷過程就是通過nextNode取出下一個節點的過程,它的核心是想就是依次遍歷哈希桶中的鏈表實現。
總結
到此爲止,HashMap原理分析完畢,閱讀時需要從第一部分先看,然後再看第二部分的源碼分析,具體的總結在概述和小結中都已經描述清楚,這裏不做過多贅述。這是週末看的HashMap源碼的理解,裏面有不對的對方歡迎大家留言指處,共同進步,後續會陸續將LinkedHashMap、CurrentHashMap的源碼分享出來以供參考。
趁熱打鐵接下來可以看一下我的另一片文章:面試必備2LinkedHashMap的源碼分析(基於JDK1.8)