jdk1.8 HashMap源碼講解
阿新 • • 發佈:2019-03-18
abstract bool equals 最大數 得到 next 總結 fab 為什麽
4.2 hashMap的put()
1. 開篇名義
jdk1.8中hashMap發生了一些改變,在之前的版本中hsahMap的組成是數組+鏈表的形式體現,而在1.8中則改為數組+鏈表+紅黑樹的形式實現,通過下面兩張圖來對比一下二者的不同。
jdk1.8之前的hashMap結構圖,基本對象為Entry<k,v> jdk1.8的hashMap結構圖,基本對象改為了Node<k,v>
註意:無論Entry<key,value>還是Node<key,value>都實現了 Map.Entry<K,V> 接口;
下面的內容將不在討論關於jdk1.7的內容,重點討論一下jdk1.8中hashMap的相關代碼實現
2. HashMap類概覽(實現的接口、繼承的類,以及成員變量)
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { // 序列號 private static final long serialVersionUID = 362498820763181265L; // 默認的初始容量是16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; // 默認的填充因子0.75f,可以在創建hashMap時指定 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 當桶(bucket)上的結點數大於這個值時會轉成紅黑樹 static final int TREEIFY_THRESHOLD = 8;// 當桶(bucket)上的結點數小於這個值時樹轉鏈表 static final int UNTREEIFY_THRESHOLD = 6; // 桶中結構轉化為紅黑樹對應的table的最小長度,即:當數組的長度大於64並且桶的長度大於8同時滿足時,才會觸發由鏈表變為紅黑樹 static final int MIN_TREEIFY_CAPACITY = 64;
/* ---------------- Fields -------------- */
transient Node<k,v>[] table; // 存儲元素的數組,總是2的冪次倍,由tableSizeFor(int cap)方法計算得出,思考為什麽要用transient關鍵字修飾
transient Set<map.entry<k,v>> entrySet; // 存放具體的元素的集
transient int size; // 存放元素的個數
transient int modCount; //每次擴容和更改map結構的計數器
int threshold; //臨界值 當實際大小(容量*填充因子)超過臨界值時,會進行擴容
final float loadFactor; //hashTable的填充因子,用於實際參與運算的加載因子
}
3. HashMap構造函數
3.1 HashMap的空參構造函數
/** * HashMap的空參構造函數,該函數就做了一件事: * 初始化轉載因子,經默認的轉載因子(0.75)賦值給loadFactor */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
3.2 HashMap的單參構造函數
/** * 單參數構造函數,指定hashMap的初始數組長度值 * */ public HashMap(int initialCapacity) { //調用本類中(int,float)構造函數,測試轉載因子為默認值 this(initialCapacity, DEFAULT_LOAD_FACTOR); }
3.3 HashMap(int ,fioat)型構造函數
public HashMap(int initialCapacity, float loadFactor) { //指定初始容量和轉載因子 // 初始容量不能小於0,否則報錯 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); // 初始容量不能大於最大值(1<<30),否則為最大值 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; // 填充因子不能小於或等於0,不能為非數字 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // 初始化填充因子 this.loadFactor = loadFactor; // 初始化threshold大小,根據傳入值的大小重新計算初始hashMap數組的長度臨界值 this.threshold = tableSizeFor(initialCapacity); }
3.4 HashMap(Map<? extends K, ? extends V> m)型構造函數
/** * 根據傳人的Map映射構造一個新的HashMap */ public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
//putMapEntries函數會將傳進的map放到新的hashMap中
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { // 判斷table是否已經初始化 if (table == null) { // pre-size // 未初始化,s為m的實際元素個數 float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); // 計算得到的t大於閾值,則初始化閾值 if (t > threshold) threshold = tableSizeFor(t); } // 已初始化,並且m元素個數大於閾值,進行擴容處理 else if (s > threshold) resize(); // 將m中的所有元素添加至HashMap中 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); } } }
3.5 總結
-
- 空參構造,初始化加載因子為默認值
- 單參數構造,指定數組初始長度(並非實際的長度,實際長度會根據tableSizeFor(int cap)函數進行計算,該構造函數自動調用雙參數構造函數
- 雙參構造函數,指定數組的實際長度(通過計算後直接獲得)和加載因子;
- 傳人鍵值對映射
註意:除了最後一種構造函數調用了putVal()方法進行了數組創建之外,其他構造方法只是在維護成員屬性,並沒有實際進行創建,創建數組實在putVal()方法中執行的
4.核心方法
4.1 根據初始值計算Map數組的初始長度
/** * Returns a power of two size for the given target capacity.
* 返回大於給定值的最小的2的倍數 */ static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; //讓當前數據的最高位非0值補全低位數據,讓高位參數到運算中,減少hash碰撞的產生 n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
思考:
- 首先為什麽cap-1?
這是為了防止,cap已經是2的冪。如果cap已經是2的冪, 又沒有執行這個減1操作,則執行完後面的幾條無符號右移操作之後,返回的capacity將是這個cap的2倍。
- 為什麽是分別後移1、2、4、8 、16?
正好是倍數關系,這樣正好能保證比原始數據小的低位全部變為1,並且保證了效率,第一次移動一位後前兩位變成11,再移動四位正好能將剛才的兩位數再降位變成1111,再移動4位 正好將1111繼續降位到後邊
- 為什麽不繼續無符號右移32位?
因為hashMap的最大數為1<<30,經過上面的多次移動,一共移動了1+2+4+8+16,已經超過了30位
4.2 hashMap的put()
1 public V put(K key, V value) { 2 return putVal(hash(key), key, value, false, true); //在putVal()方法中進行第一次的數組創建 3 }
4.3 putVal()源碼(最核心的方法)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 步驟①:tab為空則創建 // table未初始化或者長度為0,進行擴容 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 步驟②:計算index,並對null做處理 // (n - 1) & hash 確定元素存放在哪個桶中,桶為空,新生成結點放入桶中(此時,這個結點是放在數組中) if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 桶中已經存在元素 else { Node<K,V> e; K k; // 步驟③:節點key存在,直接覆蓋value // 比較桶中第一個元素(數組中的結點)的hash值相等,key相等 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 將第一個元素賦值給e,用e來記錄 e = p; // 步驟④:判斷該鏈為紅黑樹 // hash值不相等,即key不相等;為紅黑樹結點 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; } // 判斷鏈表中結點的key值與插入的元素的key值是否相等 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 相等,跳出循環 break; // 用於遍歷桶中的鏈表,與前面的e = p.next組合,可以遍歷鏈表 p = e; } } // 表示在桶中找到key值、hash值與插入元素相等的結點 if (e != null) { // 記錄e的value V oldValue = e.value; // onlyIfAbsent為false或者舊值為null if (!onlyIfAbsent || oldValue == null) //用新值替換舊值 e.value = value; // 訪問後回調 afterNodeAccess(e); // 返回舊值 return oldValue; } } // 結構性修改 ++modCount; // 步驟⑥:超過最大容量 就擴容 // 實際大小大於閾值則擴容 if (++size > threshold) resize(); // 插入後回調 afterNodeInsertion(evict); return null; }
- 判斷鍵值對數組table[i]是否為空或為null,否則執行resize()進行擴容;
- 根據鍵值key計算hash值得到插入的數組索引i,如果table[i]==null,直接新建節點添加,轉向⑥,如果table[i]不為空,轉向③;
- 判斷table[i]的首個元素是否和key一樣,如果相同直接覆蓋value,否則轉向④,這裏的相同指的是hashCode以及equals;
- 判斷table[i] 是否為treeNode,即table[i] 是否是紅黑樹,如果是紅黑樹,則直接在樹中插入鍵值對,否則轉向⑤;
- 遍歷table[i],判斷鏈表長度是否大於8,大於8的話把鏈表轉換為紅黑樹,在紅黑樹中執行插入操作,否則進行鏈表的插入操作;遍歷過程中若發現key已經存在直接覆蓋value即可;
- 插入成功後,判斷實際存在的鍵值對數量size是否超多了最大容量threshold,如果超過,進行擴容。
------------------未完待續------------------------------
jdk1.8 HashMap源碼講解