【JDK】JDK源码分析-HashMap(1)
概述
?
HashMap 是 Java 開(kāi)發(fā)中最常用的容器類之一,也是面試的常客。它其實(shí)就是前文「數(shù)據(jù)結(jié)構(gòu)與算法筆記(二)」中「散列表」的實(shí)現(xiàn),處理散列沖突用的是“鏈表法”,并且在 JDK 1.8 做了優(yōu)化,當(dāng)鏈表長(zhǎng)度達(dá)到一定數(shù)量時(shí)會(huì)把鏈表轉(zhuǎn)為紅黑樹。
?
因此,JDK 1.8 中的 HashMap 實(shí)現(xiàn)可以理解為「數(shù)組 + 鏈表 + 紅黑樹」。內(nèi)部結(jié)構(gòu)示意圖:
?
HashMap 的繼承結(jié)構(gòu)和類簽名如下:
?
public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {}PS: 還記得以前初讀 HashMap 源碼時(shí),用了周末兩天的時(shí)間,而且讀完腦子里還是一頭霧水。當(dāng)時(shí)也沒(méi)做什么筆記,這次記錄一下。
?
代碼分析
?
一些成員變量
// 默認(rèn)初始化容量(必須是 2 的次冪) static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16// 最大容量(必須是 2 的次冪,且小于等于 2^30) static final int MAXIMUM_CAPACITY = 1 << 30;// 默認(rèn)負(fù)載因子 static final float DEFAULT_LOAD_FACTOR = 0.75f;// 將鏈表轉(zhuǎn)為樹的閾值(當(dāng) bin 的數(shù)量大于等于該值時(shí),將鏈表轉(zhuǎn)為樹) // 該值必須大于 2 且至少是 8, static final int TREEIFY_THRESHOLD = 8;// 將樹轉(zhuǎn)為鏈表的閾值 static final int UNTREEIFY_THRESHOLD = 6;?
Node 類
?
先看 HashMap 中的一個(gè)嵌套類 Node,如下(部分方法省略):
/*** Basic hash bin node, used for most entries. (See below for* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)*/ 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 int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);}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 類實(shí)現(xiàn)了 Map.Entry 接口,是 HashMap 中基本的 bin 節(jié)點(diǎn),此外還有 TreeNode。參考上面的結(jié)構(gòu)圖。
?
構(gòu)造器
?
構(gòu)造器 1:無(wú)參數(shù)構(gòu)造器
// 負(fù)載因子 final float loadFactor;/*** Constructs an empty HashMap with the default initial capacity* (16) and the default load factor (0.75).*/ public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }通過(guò)注釋可知,該構(gòu)造器使用默認(rèn)的初始化容量(16)和默認(rèn)的負(fù)載因子(0.75)構(gòu)造了一個(gè)空的 HashMap。
?
構(gòu)造器 2、3:
// 使用指定的初始化容量和默認(rèn)負(fù)載因子(0.75)構(gòu)造一個(gè)空的 HashMap public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR); }// 擴(kuò)容的閾值(容量 * 負(fù)載因子) int threshold;// 使用指定的初始化容量和負(fù)載因子構(gòu)造一個(gè)空的 HashMap public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;// PS: 負(fù)載因子可以大于 1if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);this.loadFactor = loadFactor;this.threshold = tableSizeFor(initialCapacity); }?
可以看到,這兩個(gè)構(gòu)造器實(shí)質(zhì)上是同一個(gè)。值得注意的是構(gòu)造器中用到了一個(gè) tableSizeFor 方法對(duì)初始化容量(initialCapacity)進(jìn)行了處理:
/*** Returns a power of two size for the given target capacity.*/ static final int tableSizeFor(int cap) {int n = cap - 1;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; }該方法的作用的是對(duì)給定的容量 cap 進(jìn)行處理,把它轉(zhuǎn)為大于等于 cap 的 2 次冪的數(shù)字。例如:
若給定 cap 為 5,則返回是 8 (2^3);
若給定 cap 為 8,返回還是 8 (2^3);
若給定 cap 為 12,則返回是?16 (2^4).
而且,這里賦值的是 threshold 變量,即閾值。
?
構(gòu)造器 4:
// 使用指定的 Map 構(gòu)造一個(gè) HashMap,默認(rèn)負(fù)載因子為 0.75,容量充足 public HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false); }通過(guò)構(gòu)造器可以看到,創(chuàng)建一個(gè) HashMap 的時(shí)候,其內(nèi)部只是初始化了一些變量,并未分配空間。
?
常用&核心方法
?
接下來(lái)分析最常用,也是 HashMap 的核心方法:put、get 和 resize 方法。
?
put 方法:
public V put(K key, V value) {return putVal(hash(key), key, value, false, true); }該方法首先會(huì)對(duì) key 做一個(gè)處理,即 hash(key) 方法,如下:
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }該方法獲取 key 的 hashCode,并且將其 hashCode 與右移 16 位后的值做“異或(^)”處理。這一步的目的是什么?先看一個(gè)該操作的例子:
hashCode 是一個(gè) 32 位的整數(shù),將其無(wú)符號(hào)右移 16 位之后,它的高 16 位就全部變成了 0,再與它的 hashCode 做異或運(yùn)算之后,hashCode 的高 16 位不變,而低 16 位也以某種形式保留了高 16 位的信息。這樣做目的是增大低位數(shù)字的隨機(jī)性,從而盡可能減少散列沖突。
?
此處可參考:https://www.zhihu.com/question/20733617/answer/111577937
?
下面的代碼將前面生成的 hash 值和數(shù)組的長(zhǎng)度減一(n?- 1)做了一個(gè)按位與操作(相當(dāng)于對(duì)?n?- 1 取余數(shù),位操作效率更高),從而確定元素的位置。相當(dāng)于散列表的散列函數(shù)。
?
繼續(xù)分析 put 方法:
// 散列表數(shù)組 transient Node<K,V>[] table;final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;// 若 table 為空,則調(diào)用 resize 方法初始化if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 若要存放的 bin 位置為空,則直接插入到該節(jié)點(diǎn)if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);// 要存放的 bin 的位置不為空(即散列沖突)else {Node<K,V> e; K k;// key 已存在if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;// p 是樹節(jié)點(diǎn)(已經(jīng)轉(zhuǎn)成了紅黑樹),將新節(jié)點(diǎn)插入到紅黑樹中else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);// 不是樹節(jié)點(diǎn),新增元素后可能需要轉(zhuǎn)為紅黑樹else {for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);// 大于等于樹化的閾值后,將鏈表轉(zhuǎn)為紅黑樹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;}}// 處理 key 已存在的情況if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value; // 替換舊值 afterNodeAccess(e);return oldValue;}}++modCount;// 若超過(guò)閾值(capacity * 0.75),則進(jìn)行擴(kuò)容if (++size > threshold)resize();afterNodeInsertion(evict);return null; }?
其中:
1. 涉及紅黑樹的相關(guān)操作可參考「JDK源碼分析-TreeMap(2)」有關(guān) TreeMap 分析以及前文的紅黑樹;
2.?有兩個(gè)方法 afterNodeAccess(e) 和 afterNodeInsertion(evict) 是用于 LinkedHashMap (HashMap 的子類) 的回調(diào)方法,這里暫不分析。
?
put 方法操作流程如圖所示:
?
下面分析 resize 方法,該方法也是 HashMap 擴(kuò)容的核心方法:
// 初始化 table 或者對(duì)其進(jìn)行擴(kuò)容 final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;// 原 table 不為空if (oldCap > 0) {// 若 table 容量大于最大值,則將閾值調(diào)整為 Integer.MAX_VALUE,不擴(kuò)容if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}// 新容量擴(kuò)大為原先的 2 倍// 若翻倍后的容量小于 int 最大值,且原容量大于等于默認(rèn)初始容量(16),將閾值擴(kuò)大為原先的 2 倍else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold }// 用閾值替代初始容量(指定初始容量的構(gòu)造器)else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;// 無(wú)參構(gòu)造器(默認(rèn)的容量和閾值)else { // zero initial threshold signifies using defaultsnewCap = 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;// 創(chuàng)建一個(gè)新的數(shù)組(大小為擴(kuò)容后的容量大小)@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;// 原數(shù)組不為空,則進(jìn)行擴(kuò)容if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null;// 該位置只有一個(gè)元素,將該元素移到新的位置if (e.next == null)newTab[e.hash & (newCap - 1)] = e;// 該位置是紅黑樹結(jié)構(gòu),將樹節(jié)點(diǎn)拆分或轉(zhuǎn)為鏈表else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);// 該位置是鏈表結(jié)構(gòu)else { // preserve orderNode<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;// 原索引位置(注意 oldCap 是 2 的次冪,因此其 2 進(jìn)制表示只有一位是 1,其他全是 0)if ((e.hash & oldCap) == 0) {// 鏈表為空if (loTail == null)loHead = e;// 新節(jié)點(diǎn)添加到上個(gè)節(jié)點(diǎn)末尾elseloTail.next = e;loTail = e;}// 原索引位置+oldCapelse {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);// 設(shè)置 j 和 oldCap+j 位置的頭結(jié)點(diǎn)if (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab; }?
擴(kuò)容后的新容量為原先的 2 倍,下面分析其擴(kuò)容的原理:
擴(kuò)容前:
原容量為 16,key1 和 key2 對(duì)應(yīng)的 hash 值只有倒數(shù)第 5 位不同,此時(shí)對(duì) oldCap-1 (15) 執(zhí)行按位與操作,二者得到的結(jié)果都是 1111,都存放在第 15 個(gè)位置;
?
擴(kuò)容后的位置選擇:
代碼中的判斷條件為:if((e.hash & oldCap) == 0),也就是將 hash1 和 hash2 分別與 oldCap?(16, 0b10000) 進(jìn)行按位與操作,根據(jù)其是否為 0 來(lái)決定它在擴(kuò)容后的新數(shù)組中的位置。可以看到倒數(shù)第五位中,key1 是 0,key2 是1.
?
擴(kuò)容后:
新容量為 32,原 hash 值倒數(shù)第 5 位為 0 的 key1?在新數(shù)組中的位置仍是 15 (0b1111),而原 hash 值倒數(shù)第五位為 1 的 key2 在新數(shù)組中的位置是 0b11111,即 15 + 16 = 31.
?
如圖所示:
get 方法
?
前面分析了 put 方法,get 方法有不少地方與之類似,因此分析起來(lái)就簡(jiǎn)單不少。代碼如下:
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;if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {// 第一個(gè)節(jié)點(diǎn)即為要找的元素if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))return first;// 該位置有后序節(jié)點(diǎn)(為鏈表或紅黑樹)if ((e = first.next) != null) {// 若是樹節(jié)點(diǎn),說(shuō)明該位置是紅黑樹,在紅黑樹中查找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; }?
小結(jié)
?
本文主要分析了 HashMap 的內(nèi)部結(jié)構(gòu),以及最核心的三個(gè)方法:put、resize 和 get 方法。小結(jié)如下:
?
1. HashMap 是散列表的實(shí)現(xiàn),它使用“鏈表法”處理散列沖突用,并在 JDK 1.8 引入紅黑樹進(jìn)一步優(yōu)化;
2. 內(nèi)部結(jié)構(gòu)為「數(shù)組 + 鏈表 + 紅黑樹」;
3. 默認(rèn)初始化容量為 16,負(fù)載因子為 0.75,擴(kuò)容的閾值為 16 * 0.75 = 12;
4. 當(dāng)容器中元素的容量大于閾值時(shí),HashMap 會(huì)自動(dòng)擴(kuò)容為原先的 2 倍。
?
參考文章:
https://tech.meituan.com/2016/06/24/java-hashmap.html
?
相關(guān)閱讀:
數(shù)據(jù)結(jié)構(gòu)與算法筆記(二)
JDK源碼分析-TreeMap(2)
?
Stay hungry, stay foolish.
PS: 本文首發(fā)于微信公眾號(hào)。
轉(zhuǎn)載于:https://www.cnblogs.com/jaxer/p/11117778.html
總結(jié)
以上是生活随笔為你收集整理的【JDK】JDK源码分析-HashMap(1)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 初使用swagger遇到的问题(1)
- 下一篇: CF623E Transforming