HashMap原始碼解析jdk1.8:初始化resize,新增put,獲取get
阿新 • • 發佈:2019-01-10
原始碼解析有參考以下部落格:
HashMap:
以k-v鍵值對儲存格式的容器,key,value都可以為空,key不重複,非執行緒安全(執行緒安全請使用ConcurrentHashMap);
底層採用的是 陣列+(連結串列 / 紅黑樹)結構組成; 常用的有put(),get(),size(),remove(),entrySet()等方法。
下面是對:初始化resize,put,get的原始碼解析,都在原始碼中每一步中均有註釋說明
初始化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; // 新陣列大小和容量 // 1.舊陣列大小大於0 (新陣列大小及容量均擴大兩倍 或 新陣列大小擴大兩倍 或 無需擴容) if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { // 若舊陣列大小>=最大容量 threshold = Integer.MAX_VALUE; // 陣列容量為Integer最大值 return oldTab; } // 新陣列大小擴大二倍, else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) // 若newCap小於最大容量, 舊陣列大小>=16 則新容量擴大二倍 newThr = oldThr << 1; // double threshold } // 2.舊陣列容量大於0 (新陣列大小等於舊容量,未涉及新容量) else if (oldThr > 0) // initial capacity was placed in threshold // 新大小 = 舊容量 newCap = oldThr; // 3.使用預設值(新陣列大小預設值16,新容量=16*0.75(預設負載因子)=12) else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 若新陣列容量=0(1和2 會導致newThr==0) if (newThr == 0) { // 新容量預期值 = 新陣列大小*負載因子 float ft = (float)newCap * loadFactor; // 若新陣列大小和ft都小於最大容量 則新容量=ft,否則等於Integer最大值 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } // 容量的值 = 新容量 threshold = newThr; // newCap值為新陣列大小建立node陣列 @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // table引用newTab table = newTab; if (oldTab != null) { // 省略不做分析 :在新陣列中新增舊資料 } }
新增元素put
1.計算hash值並呼叫putVal方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
2.判斷陣列是否為空,如果為空,初始化table陣列
3.新增資料
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { // tab是指將要操作的陣列引用 // p表示tab上hash值對應的Node節點 // n表示tab長度,i表示p在tab下標 Node<K,V>[] tab; Node<K,V> p; int n, i; // table為儲存資料的陣列 if ((tab = table) == null || (n = tab.length) == 0) // 2.初始化 n = (tab = resize()).length; // 3.新增資料 // 判斷key所在的Node陣列元素p是否為空 if ((p = tab[i = (n - 1) & hash]) == null) // 3.1 p == null 只需新建一個Node即可 tab[i] = newNode(hash, key, value, null); else { // e: p中node節點(表示每次p.next的Node),最終表示即將新增的key,value // k: p中node節點(e)的key HashMap.Node<K,V> e; K k; // 3.2 p != null if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 3.2.1 p第一個節點的key就等於新增的key,直接返回p e = p; else if (p instanceof HashMap.TreeNode) // 3.2.2 p是紅黑樹----todo e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 3.2.3 遍歷連結串列結構的p,每次獲取p的下一個節點Node,並記錄binCount(標記用於轉換紅黑樹) for (int binCount = 0; ; ++binCount) { // 3.2.4 p.next == null if ((e = p.next) == null) { // p.next 表示獲取p的下一個Node // 表示已經到達節點末尾,只需要建立key,value的Node物件,並將p.nexc指向它,退出迴圈 p.next = newNode(hash, key, value, null); // 若p節點大於等於8,將連結串列結構轉化為紅黑樹 // (條件比較>=7,因為binCount從0開始計數的) ----todo if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } // 3.2.5 p.next.key == key (e.key等於新增的key,退出迴圈) if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; // 重置p,不然的話每次p.next都是p的第二個節點(因為e = p.next,加上p = e,就相當於p = p.next) p = e; } } // 3.2.6 將value替換e中的value if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; // 這個函式在hashmap中沒有任何操作,是個空函式,他存在主要是為了linkedHashMap的一些後續處理工作。 // 引用http://www.cnblogs.com/jzb-blog/p/6637823.html 原話 afterNodeAccess(e); return oldValue; } } // 資料結構變化 ++modCount; if (++size > threshold) // 前面提到過threshold是陣列容量 若超過需要擴容 resize(); // 這裡與前面的afterNodeAccess同理,是用於linkedHashMap的尾部操作,HashMap中並無實際意義。 // 引用http://www.cnblogs.com/jzb-blog/p/6637823.html 原話 afterNodeInsertion(evict); return null; }
獲取get
通過hash值找到在table陣列中的那個Node節點(該節點存有大於等於8個元素,則轉化為紅黑樹,發生在put操作),
紅黑樹:通過樹形結構查詢 連結串列:迴圈比較連結串列每一個Node的key
final HashMap.Node<K,V> getNode(int hash, Object key) {
// table陣列 first:hash值對應的Node e:first中的其中一個
// n:tab陣列長度 k:first中的key
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> first, e; int n; K k;
// 1.查詢hash值對應的node節點 為null直接返回null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 2.該hash位置的Node第一個key等於需要查詢的key 返回該節點
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 3.獲取first下一個node
if ((e = first.next) != null) {
// 3.1 若是樹 通過樹形結構獲取key對應的Node
if (first instanceof HashMap.TreeNode)
return ((HashMap.TreeNode<K,V>)first).getTreeNode(hash, key);
// 3.2 否則迴圈比較first連結串列每一個Node的key
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
面試必問的原始碼問題,今天抽出時間也把原始碼理解以下併發布文章。希望有描述不準確,理解不到位的地方,
非常感謝各位,能夠指點一下,有您的指點我定會更加深刻,非常感謝花費您寶貴的時間看我這篇文章。
再次註明參考部落格: