java集合系列之HashMap源碼
阿新 • • 發佈:2018-06-30
實現 幫助 成員變量 eno dea after 一次 == 處的
java集合系列之HashMap源碼
HashMap的源碼可真不好消化!!!
首先簡單介紹一下HashMap集合的特點。HashMap存放鍵值對,鍵值對封裝在Node(代碼如下,比較簡單,不再介紹)節點中,Node節點實現了Map.Entry。存放的鍵值對的鍵不可重復。jdk1.8後,HashMap底層采用的是數組加鏈表、紅黑樹的數據結構,因此實現起來比之前復雜的多。
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V 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() { returnvalue; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(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; } }
下面是我對HashMap源碼的一點理解,除了與紅黑樹相關的操作不清楚之外,其余理解還算湊合,希望對各位有所幫助。
首先看一下它的靜態常量和成員變量:
/** *默認初始化容量16 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16 /** * 最大容量 */ 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; /** * 在轉變成樹之前,還會有一次判斷,只有鍵值對數量大於 64 才會發生轉換。 * 這是為了避免在哈希表建立初期,多個鍵值對恰好被放入了同一個鏈表中而導致不必要的轉化。 */ static final int MIN_TREEIFY_CAPACITY = 64;
/** * 存放數據的數組,數組中的元素類型為Node,Node實現了Map.Entry */ transient Node<K,V>[] table; /** * 存放所有的鍵值對對象,也可以成為Node對象,還可成為Entry對象 */ transient Set<Map.Entry<K,V>> entrySet; /** * 鍵值對的數量 */ transient int size; /** * 集合被修改的次數 */ transient int modCount; /** *下次發生數組擴容的值(capacity * loadFactor) */ int threshold; /** *加載因子 */ final float loadFactor;
HashMap的構造方法如下:
HashMap的構造方法並沒有對其(數組)進行初始化,而是在集合第一次添加元素時,才進行初始化,構造方法只是對容量和加載因子進行設置。這是一種懶加載機制(lazy_load)。
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0)//容量小於0拋異常 throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY)//超過最大容量,設為最大容量 initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor))//判斷加載因子是否非法 throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; //用於找到大於等於initialCapacity最小的2的冪數, //奇怪的是卻將這個值賦給了threshold,threashold應該賦值為tableSizeFor(initialCapacity)*loadFactor //原因是在resize方法對數組進行初始化時,重新賦值了 this.threshold = tableSizeFor(initialCapacity); } /** * 傳入初始化容量,默認加載 */ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } /** * 空參構造 */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
接下來是HashMap最終要的幾個方法。
首先是resize()方法:
用於數組初始化或數組擴容(也就是rehash過程)
/** * resize()方法兼顧兩個職責: * 1. 創建初始存儲表格(由於在HashMap的構造方法中僅僅對容量、門限值和加載因子進行了設置,並未對存儲表格進行初始化, * 存儲表格的初始化發生在put方法的初次調用,內部調用resize(),進行初始化表格) * 2. 當容量不滿足需求時進行擴容 */ final Node<K,V>[] resize() { Node<K,V>[] oldTab = table;//oldTable保留擴充前的數組 int oldCap = (oldTab == null) ? 0 : oldTab.length;//判斷是否第一次添加元素 int oldThr = threshold;//保留了擴充前的門限值 int newCap, newThr = 0;//新的容量和門限值都設為0 if (oldCap > 0) {//如果擴充前數組不為空 if (oldCap >= MAXIMUM_CAPACITY) {//如果之前定的容量已經達到最大容量, threshold = Integer.MAX_VALUE;//僅僅將門限值設為Integer的最大值即可,它大約是MAXIMUM_CAPACITY的2倍 return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&//首先將newCap設為原來的兩倍,如果擴充之前數組容量超過了最大初始化容量,並且它的2倍小於最大容量 oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // 將新的門限值設為原來的兩倍, } else if (oldThr > 0) // 這個是存儲表格初始化時執行的分支,集合創建時調用的是帶參構造 newCap = oldThr; else { // 這個也是存儲表格初始化時執行的分支,集合創建時調用的是無參構造 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) {//使用帶參構造初始化或者原容量處於默認初始化容量和二分之最大容量之外時,才成立 float ft = (float)newCap * loadFactor;//相當於新的門限值的雛形 //如果ft和新容量都沒有超過最大容量,則將新的門限值設為該門限值,否則將設為最大整數值 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?// (int)ft : Integer.MAX_VALUE); } threshold = newThr;//對集合的門限值進行重新設定 //下面是對新數組的創建和對集合中的數據添加到新數組。也就是俗稱的rehash @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//創建新的數組(存儲表格) table = newTab;//table指向新創建的數組 if (oldTab != null) {//如果原來數組不為null,即不是第一次添加元素 for (int j = 0; j < oldCap; ++j) {//遍歷數組 Node<K,V> e; if ((e = oldTab[j]) != null) {//e記錄數組該索引處中的元素 oldTab[j] = null;//將該索引處的元素置為null if (e.next == null)//如果該索引處有且僅有一個元素,即e.next == null //由於newCap都是2的指冪指數,因此newCap-1的值的二進制形式為高位為0,低位全部為1, //因此e的hash值&newCap-1的值的二進制形式為:保留了所有的低位,高位為0,因此這個值肯定小於newCap,也就是索引一定存在 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode)//如果e為紅黑樹的根節點,調用split方法對紅黑樹進行拆分 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { //如果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) {//高位為0:oldCap為2的冪指數,&運算保留了他的高位 if (loTail == null)//鏈表為空時 loHead = e; else loTail.next = e;//尾節點右移 loTail = e; } else {//高位為1:oldCap為2的冪指數,&運算保留了他的高位 if (hiTail == null)//鏈表為空 hiHead = e; else hiTail.next = e; hiTail = e;//尾節點右移 } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead;//將高位為0的節點組成鏈表的頭結點放到該索引 } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead;//將高位為1的節點組成鏈表的頭結點放到該索引平移oldCap處的索引處 } } } } } return newTab;//返回新生成的數組 }
然後是put方法,put方法內部調用putVal()
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } 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)//如果數組為null或者長度為0 n = (tab = resize()).length;//初始化數組 //計算桶的位置,由於n為2的冪指數,所以n-1的二進制位全1,所以(n - 1)&hash小於n if ((p = tab[i = (n - 1) & hash]) == null)//如果該處沒有元素,直接添加新節點 tab[i] = newNode(hash, key, value, null); else {//如果該處已經有元素 Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))//如果頭結點的hash值和equals都相同則將e指向p e = p; else if (p instanceof TreeNode)//如果p為紅黑樹,則向紅黑樹中添加元素 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; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break;//如果找到相同元素則退出循環 p = e; } } if (e != null) { // 找到相同元素,則覆蓋舊值,並返回舊值 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount;//添加了新元素,所以modCount自增 if (++size > threshold)//判斷是否需要擴容 resize(); afterNodeInsertion(evict); return null;//如果找不到返回null }
不難看出,putVal方法傳入的不是key本身的hashcode()的值,而是下面這個方法:
/** * 為什麽要有HashMap的hash()方法,難道不能直接使用KV中K原有的hash值嗎?在HashMap的put、get操作時為什麽不能直接使用K中原有的hash值。 * key的hash值高16位不變,低16位與高16位異或作為key的最終hash值。(h >>> 16,表示無符號右移16位,高位補0,任何數跟0異或都是其本身,因此key的hash值高16位不變。) * 為什麽要這麽幹呢? 這個與HashMap中table下標的計算有關。index = (n-1) & hash,n為2的冪數 */ static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
才疏學淺,只能寫到這了,該死的半天時間又過去了。源碼中遨遊,望能有所收獲。
java集合系列之HashMap源碼