Java源碼解析之HashMap的put、resize方法詳解
一、HashMap 簡介
HashMap 底層采用哈希表結構 數組加鏈表加紅黑樹實現,允許儲存null鍵和null值
數組優點:通過數組下標可以快速實現對數組元素的訪問,效率高
鏈表優點:插入或刪除數據不需要移動元素,隻需要修改節點引用效率高
二、源碼分析
2.1 繼承和實現
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { private static final long serialVersionUID = 362498820763181265L;
繼承AbstractMap<K,V>
實現瞭map接口 cloneable接口和可序列化接口
2.2 屬性
//hashmap默認容量為 16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 HashMap默認容量 16 //最大容量為2的30 1073741824 static final int MAXIMUM_CAPACITY = 1 << 30; // //默認加載因子為0.75 static final float DEFAULT_LOAD_FACTOR = 0.75f; //鏈表轉為紅黑樹的閾值 static final int TREEIFY_THRESHOLD = 8; //紅黑樹轉為鏈表的閾值 static final int UNTREEIFY_THRESHOLD = 6; //鏈表轉為紅黑樹時數組容量必須大於64 static final int MIN_TREEIFY_CAPACITY = 64; // transient Node<K,V>[] table; transient Set<Map.Entry<K,V>> entrySet; transient int size; //用於快速失敗機制 在對HashMap進行迭代時,如果期間其他線程的參與導致HashMap的結構發生變化瞭(比如put,remove等操作),直接拋出ConcurrentModificationException異常 transient int modCount; //擴容閾值,計算方法為數組容量*填充因子 int threshold; //加載因子 填充因子 final float loadFactor;
loadFactor決定數組何時進行擴容,而且為什麼是0.75f
它也叫擴容因子 比如數組長度為32,所以數組擴容閾值為32*0.75=24當數組中數據個數為24時數組進行擴容,
數組容量在創建的時候就確定,擴容時重新創建一個指定容量的數組,然後講舊數組的值復制到新數組中,擴容過程非常耗時,所以0.75時基於容量和性能之間平衡的結果。
- 如果加載因子過大,也就是擴容閾值會變大,擴容門檻高,這樣容量的占用率就會降低,但哈希碰撞的幾率就會增加,效率下降
- 如果加載因子過小,擴容閾值變小,擴容門檻低,容量占用變大但哈希碰撞幾率下降
此外用於存儲數據的table字段使用transient修飾,通過transient修飾的字段在序列化的時候將被排除在外,那麼HashMap在序列化後進行反序列化時,是如何恢復數據的呢?HashMap通過自定義的readObject/writeObject方法自定義序列化和反序列化操作。這樣做主要是出於以下兩點考慮:
1.table一般不會存滿,即容量大於實際鍵值對個數,序列化table未使用的部分不僅浪費時間也浪費空間;
2.key對應的類型如果沒有重寫hashCode方法,那麼它將調用Object的hashCode方法,該方法為native方法,在不同JVM下實現可能不同;換句話說,同一個鍵值對在不同的JVM環境下,在table中存儲的位置可能不同,那麼在反序列化table操作時可能會出錯。
所以在HashXXX類中(如HashTable,HashSet,LinkedHashMap等等),我們可以看到,這些類用於存儲數據的字段都用transient修飾,並且都自定義瞭readObject/writeObject方法。readObject/writeObject方法
2.3 節點類型Node內部類
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() { return value; } 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; } }
Node包含四個字段
final int hash; final K key; V value; Node<K,V> next;//包含鏈表下一個節點
HashMap通過hash方法計算key的哈希值,然後通過(n-1)&hash得到key在數組中存放的下標,當兩個key相同時,會以鏈地址法處理哈希碰撞
在鏈表中查找數據必須從第一個元素開始,時間復雜度為O n 所以當鏈表長度越來越長時HashMap的查詢效率就會越來越低
所以為瞭解決這個問題JDK1.8實現瞭數組+鏈表+紅黑樹來解決 當鏈表長度超過8個時並且數組長度大於64
時進行樹化,轉化後查詢時間復雜度為O(logN).
2.4 紅黑樹的節點
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // red-black tree links 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); }
包含左右孩子節點和雙親結點,和前驅節點,還有節點是否時紅或者黑
三、構造方法
3.1 構造器1
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
最常用的構造器。默認的填充因子 0.75f 這裡的填充因子後面會講到。
3.2 構造器2
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
給定容量構造器
3.3 構造器3
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))//若填充因子小於0或者判斷非法 throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } static final int tableSizeFor(int cap) { int n = cap - 1; 讓cap-1再賦值給n的目的是另找到的目標值大於或等於原值 n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
給定容量和填充因子。
這裡的tableSizeFor會將傳進的容量值進行**大於等於
最近**二次冪處理。跟循環數組的處理方式差不多
3.4 構造器4
public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
四、put
public V put(K key, V value) { //底層是調用putval return putVal(hash(key), key, value, false, true); } //這裡調用瞭hashmap提供的hash方法,32為都參與瞭運算所以降低瞭hash碰撞的幾率,這裡還跟數組容量有關 //下面再討論 static final int hash(Object key) { int h; //這裡就可以看到hashmap return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
通過hash函數可以看到當key為null時,key為0,所以HashMap 是允許儲存空值的。而後面的公式通過hashcode的高16位異或低1位得到的hash值,主要從性能、哈希碰撞角度考慮,減少系統開銷,不會因為高位沒有參與下標計算而引起的碰撞
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //請註意這裡的hash是已經算過的hash(key),然後計算數組下標位置(n - 1) & hash Node<K,V>[] tab; Node<K,V> p; int n, i; //首先判斷數組哈希表是否為null或者長度為0,是則進行數組初始化操作 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //這裡tab指向table數組 n是數組長度 //如果該數組下標位置沒有數據直接插入 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else {//該位置有元素 Node<K,V> e; K k; //首先判斷此位置的值的hash和key的地址和值是否相等 //如果相等直接覆蓋 //小問題這裡為什麼先判斷hash值而不是判斷key值,因為hash值判斷最快,如果hash值不同就不用判斷下面的 //hash不同則key一定不同,但key相同hash值是可能相同的,效率提高 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //否則就是該位置可以不存在,如果該節點是紅黑樹類型, 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,則轉為紅黑樹 //這裡為什麼要-1,因為數組下標從0開始 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //轉為紅黑樹操作時,內部還會判斷數組長度是否小於MIN_TREEIFY_CAPACITY 64,如果是的話不轉換 treeifyBin(tab, hash); break;//退出 } //如果鏈表中已經存在該key,直接覆蓋 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; //遍歷數組 e = p.next p = e; } } //e代表被覆蓋的值 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; // 如果onlyIfAbsent為false並且oldValue為null,我們便對我們的value進行保存 afterNodeAccess(e); return oldValue; } } ++modCount; //如果鍵值對個數大於擴容閾值,進行擴容操作 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
流程圖
五、get
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //如果桶為空,size為0,目標位置是否為空,是直接返回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; //否則如果頭節點的next有值 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; }
六、resize
數組的擴容和初始化都要靠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; //如果擴容前數組 if (oldCap > 0) { //如果超過最大值就不用再擴容 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //2倍擴容不能大於最大值 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) //擴容閾值/2 newThr = oldThr << 1; // double threshold } //數組中沒有值,帶參初始化會進入這裡 //且擴容因子大於0 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; //不帶參默認會到這裡 else {//否則擴充因子 <= 0 //就是沒初始化過,使用默認的初始化容量,16 * 0.75 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } //如果新容量為0,重新計算threshold擴容閾值 if (newThr == 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) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; //如果當前節點鏈表數據隻有一個,則直接賦值 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) //否則紅黑樹操作 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order //鏈表賦值 高低映射 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) { //相同則低位鏈表尾插 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) { loTail.next = null; newTab[j] = loHead; } //高位引用 直接講原索引+oldCap放到哈希桶中 //因為是2倍擴容, 擴容後位置是原位置+增長的長度 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
七、基於JDK1.7的優化
7.1 底層實現
1.7基於數組+鏈表 而1.8基於鏈表+數組+紅黑樹
7.2 hash
final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
效率基於1.8低
7.3 put
1.7使用的是數組加鏈表,解決哈希沖突采用的是鏈表,而且1.8采用的是尾插,而1.7采用頭插
7.4 擴容
1.7在擴容時會重新計算h每個元素的hash值,按舊鏈表的正序遍歷鏈表,然後在新鏈表的頭部插入,所以會出現逆序的情況,而1.8是通過高低位映射,不會出現逆序。
到此這篇關於Java源碼解析之HashMap的put、resize方法詳解的文章就介紹到這瞭,更多相關HashMap的put、resize方法內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- None Found