入門(mén)JDK集合之HashMap解析
0.前言
HashMap簡(jiǎn)述:
- HashMap 基于哈希表的 Map 接口實(shí)現(xiàn),是以 key-value 存儲(chǔ)形式存在,即主要用來(lái)存放鍵值對(duì)。HashMap 的實(shí)現(xiàn)不是同步的,這意味著它不是線(xiàn)程安全的。它的 key、value 都可以為 null,此外,HashMap 中的映射不是有序的。
- jdk1.8 之前 HashMap 由 數(shù)組 + 鏈表 組成,數(shù)組是 HashMap 的主體,鏈表則是主要為了解決哈希沖突(兩個(gè)對(duì)象調(diào)用的 hashCode 方法計(jì)算的哈希值經(jīng)哈希函數(shù)算出來(lái)的地址被別的元素占用)而存在的(“拉鏈法”解決沖突)。jdk1.8 以后在解決哈希沖突時(shí)有了較大的變化,當(dāng)鏈表長(zhǎng)度大于閾值(或者紅黑樹(shù)的邊界值,默認(rèn)為 8 )并且當(dāng)前數(shù)組的長(zhǎng)度大于 64 時(shí),此時(shí)此索引位置上的所有數(shù)據(jù)改為使用紅黑樹(shù)存儲(chǔ)。
HashMap擴(kuò)容機(jī)制簡(jiǎn)述:
HashMap == 數(shù)組+散鏈表+紅黑樹(shù)
- HashMap 默認(rèn)初始桶位數(shù)16,如果某個(gè)桶中的鏈表長(zhǎng)度大于8,則先進(jìn)行判斷:
- 如果桶位數(shù)小于64,則先進(jìn)行擴(kuò)容(2倍),擴(kuò)容之后重新計(jì)算哈希值,這樣桶中的鏈表長(zhǎng)度就變短了(之所以鏈表長(zhǎng)度變短與桶的定位方式有關(guān),請(qǐng)接著往下看)。
- 如果桶位數(shù)大于64,且某個(gè)桶中的鏈表長(zhǎng)度大于8,則對(duì)鏈表進(jìn)行樹(shù)化(紅黑樹(shù),即自平衡的二叉樹(shù))
- 如果紅黑樹(shù)的節(jié)點(diǎn)數(shù)小于6,樹(shù)也會(huì)重新變會(huì)鏈表。
所以得出樹(shù)化條件:鏈表閾值大于8,且桶位數(shù)大于64(數(shù)組長(zhǎng)度),才進(jìn)行樹(shù)化。
元素放入桶(數(shù)組)中,定位桶的方式:通過(guò)數(shù)組下標(biāo) i 定位,添加元素時(shí),目標(biāo)桶位置 i 的計(jì)算公式,i = hash & (cap - 1),cap為容量。
為什么優(yōu)先擴(kuò)容桶位數(shù)(數(shù)組長(zhǎng)度),而不是直接樹(shù)化?
- 這樣做的目的是因?yàn)?,?dāng)桶位數(shù)(數(shù)組長(zhǎng)度)比較小時(shí),應(yīng)盡量避開(kāi)紅黑樹(shù)結(jié)構(gòu),這種情況下變?yōu)榧t黑樹(shù)結(jié)構(gòu),反而會(huì)降低效率。因?yàn)榧t黑樹(shù)需要逬行左旋,右旋,變色這些操作來(lái)保持平衡。同時(shí)數(shù)組長(zhǎng)度小于64時(shí),搜索時(shí)間相對(duì)要快些。所以結(jié)上所述為了提高性能和減少搜索時(shí)間,底層閾值大于8并且數(shù)組長(zhǎng)度大于64時(shí),鏈表才轉(zhuǎn)換為紅黑樹(shù),具體可以參考下文要講述的 treeifyBin() 方法。
- 而當(dāng)閾值大于 8 并且數(shù)組長(zhǎng)度大于 64 時(shí),雖然增了紅黑樹(shù)作為底層數(shù)據(jù)結(jié)構(gòu),結(jié)構(gòu)變得復(fù)雜了,但是,長(zhǎng)度較長(zhǎng)的鏈表轉(zhuǎn)換為紅黑樹(shù)時(shí),效率也變高了。
HashMap 特點(diǎn):
- 存儲(chǔ)無(wú)序;
- 鍵和值位置都可以是 null,但是鍵位置只能存在一個(gè) null;
- 鍵位置是唯一的,是由底層的數(shù)據(jù)結(jié)構(gòu)控制的;jdk1.8 前數(shù)據(jù)結(jié)構(gòu)是鏈表+數(shù)組,jdk1.8 之后是鏈表+數(shù)組+紅黑樹(shù);
- 閾值(邊界值)> 8 并且桶位數(shù)(數(shù)組長(zhǎng)度)大于 64,才將鏈表轉(zhuǎn)換為紅黑樹(shù),變?yōu)榧t黑樹(shù)的目的是為了高效的查詢(xún);
1.HashMap存儲(chǔ)數(shù)據(jù)的過(guò)程
注意:相對(duì)于直接去讀HashMap源碼來(lái)說(shuō),先debug一下其執(zhí)行數(shù)據(jù)存儲(chǔ)的流程,更方便大家理解!
測(cè)試代碼:
@Test public void test01() { HashMap<String, Integer> hashMap = new HashMap(); hashMap.put("a", 3); hashMap.put("b", 4); hashMap.put("c", 5); hashMap.put("a", 88888);// 修改 System.out.println(hashMap); }
輸出結(jié)果:
{a=88888, b=456, c=789}
執(zhí)行流程分析:
1.首先,HashMap<String, Integer> hashMap = new HashMap();
當(dāng)創(chuàng)建 HashMap 集合對(duì)象的時(shí)候,HashMap 的構(gòu)造方法并沒(méi)有創(chuàng)建數(shù)組,而是在第一次調(diào)用 put 方法時(shí)創(chuàng)建一個(gè)長(zhǎng)度是16 的數(shù)組(即,16個(gè)桶) ,Node[] table
(jdk1.8 之前是 Entry[] table)用來(lái)存儲(chǔ)鍵值對(duì)數(shù)據(jù)。
2.當(dāng)向哈希表中存儲(chǔ)put("a", 3)
的數(shù)據(jù)時(shí),根據(jù)"a"
字符串調(diào)用 String 類(lèi)中重寫(xiě)之后的 hashCode() 方法計(jì)算出哈希值,然后結(jié)合數(shù)組長(zhǎng)度(桶數(shù)量)采用某種算法計(jì)算出向 Node 數(shù)組中存儲(chǔ)數(shù)據(jù)的空間索引值(比如table[i],
這里的i就是該Node數(shù)組的空間索引)。如果計(jì)算出的索引空間沒(méi)有數(shù)據(jù)(即,這個(gè)桶是空的),則直接將<"a", 3>
存儲(chǔ)到數(shù)組中。
舉例:如果計(jì)算出的索引是 3,則存儲(chǔ)到如下桶位:
3.當(dāng)向哈希表中存儲(chǔ)數(shù)據(jù)<"b", 4>
時(shí),假設(shè)算出的 hashCode() 方法結(jié)合數(shù)祖長(zhǎng)度計(jì)算出的索引值也是3,那么此時(shí)數(shù)組空間不是 null(即,這個(gè)桶目前不為空),此時(shí)底層會(huì)比較 "a"
和 "b"
的 hash 值是否一致,如果不一致,則在空間上劃出一個(gè)結(jié)點(diǎn)來(lái)存儲(chǔ)鍵值對(duì)數(shù)據(jù)對(duì) <"b", 4>
,這種方式稱(chēng)為拉鏈法。
4.當(dāng)向哈希表中存儲(chǔ)數(shù)據(jù)<"a", 88888>
時(shí),那么首先根據(jù) "a"
調(diào)用 hashCode() 方法結(jié)合數(shù)組長(zhǎng)度計(jì)算出索引肯定是 3,此時(shí)比較后存儲(chǔ)的數(shù)據(jù)"a"
和已經(jīng)存在的數(shù)據(jù)的 hash 值是否相等,如果 hash 值相等,此時(shí)發(fā)生哈希碰撞。那么底層會(huì)調(diào)用 "a"所屬類(lèi) String 中的 equals() 方法比較兩個(gè)內(nèi)容是否相等:
相等:將后添加的數(shù)據(jù)的 value 覆蓋之前的 value。
不相等:繼續(xù)向下和其他的數(shù)據(jù)的 key 進(jìn)行比較,如果都不相等,則劃出一個(gè)結(jié)點(diǎn)存儲(chǔ)數(shù)據(jù),如果結(jié)點(diǎn)長(zhǎng)度即鏈表長(zhǎng)度大于閾值 8 并且數(shù)組長(zhǎng)度大于 64 則將鏈表變?yōu)榧t黑樹(shù)。
5.在不斷的添加數(shù)據(jù)的過(guò)程中,會(huì)涉及到擴(kuò)容問(wèn)題,當(dāng)超出閾值(且要存放的位置非空)時(shí),擴(kuò)容。默認(rèn)的擴(kuò)容方式:擴(kuò)容為原來(lái)容量的 2 倍,并將原有的數(shù)據(jù)復(fù)制過(guò)來(lái)。
6.綜上描述,當(dāng)位于一個(gè)表中的元素較多,即 hash 值相等但是內(nèi)容不相等的元素較多時(shí),通過(guò) key 值依次查找的效率較低。而 jdk1.8 中,哈希表存儲(chǔ)采用數(shù)組+鏈表+紅黑樹(shù)實(shí)現(xiàn),當(dāng)鏈表長(zhǎng)度(閾值)超過(guò)8且當(dāng)前數(shù)組的長(zhǎng)度大于64時(shí),將鏈表轉(zhuǎn)換為紅黑樹(shù),這樣大大減少了查找時(shí)間。
簡(jiǎn)單的來(lái)說(shuō),哈希表是由數(shù)組+鏈表+紅黑樹(shù)(JDK1.8增加了紅黑樹(shù)部分)實(shí)現(xiàn)的。如下圖所示:
7.jdk1.8 中引入紅黑樹(shù)的進(jìn)一步原因:
- jdk1.8 以前 HashMap 的實(shí)現(xiàn)是數(shù)組+鏈表,即使哈希函數(shù)取得再好,也很難達(dá)到元素百分百均勻分布。當(dāng) HashMap 中有大量的元素都存放到同一個(gè)桶中時(shí),這個(gè)桶下有一條長(zhǎng)長(zhǎng)的鏈表,這個(gè)時(shí)候 HashMap 就相當(dāng)于一個(gè)單鏈表,假如單鏈表有n個(gè)元素,遍歷的時(shí)間復(fù)雜度就是O(n),完全失去了它的優(yōu)勢(shì)。
- 針對(duì)這種情況,jdk1.8 中引入了紅黑樹(shù)(查找時(shí)間復(fù)雜度為 O(logn))來(lái)優(yōu)化這個(gè)問(wèn)題。當(dāng)鏈表長(zhǎng)度很小的時(shí)候,即使遍歷,速度也非???,但是當(dāng)鏈表長(zhǎng)度不斷變長(zhǎng),肯定會(huì)對(duì)查詢(xún)性能有一定的影響,所以才需要轉(zhuǎn)成樹(shù)。
8.總結(jié):
說(shuō)明:
- size 表示 HashMap 中鍵值對(duì)的實(shí)時(shí)數(shù)量(即,所存儲(chǔ)元素的數(shù)量),注意這個(gè)不等于數(shù)組的長(zhǎng)度。
- threshold(臨界值)= capacity(容量)* loadFactor(負(fù)載因子)。這個(gè)值是當(dāng)前已占用數(shù)組長(zhǎng)度的最大值。size 超過(guò)這個(gè)值就重新 resize(擴(kuò)容),擴(kuò)容后的 HashMap 容量是之前容量的2倍。
2.HashMap相關(guān)面試題
具體原理我們下文會(huì)具體分析,這里先大概了解下面試的時(shí)候會(huì)問(wèn)什么,帶著問(wèn)題去讀源碼,便于理解!
1.shMap 中 hash 函數(shù)是怎么實(shí)現(xiàn)的?還有哪些hash函數(shù)的實(shí)現(xiàn)方式?
答:
- 對(duì) key 的 hashCode 做 hash 操作,如果key為null則直接賦哈希值為0,否則,無(wú)符號(hào)右移 16 位然后做異或位運(yùn)算,如,代碼所示:
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
- 除上面的方法外,還有平方取中法,偽隨機(jī)數(shù)法 和 取余數(shù)法。這三種效率都比較低,而無(wú)符號(hào)右移 16 位異或運(yùn)算效率是最高的。
2.當(dāng)兩個(gè)對(duì)象的 hashCode 相等時(shí)會(huì)怎么樣?
答:會(huì)產(chǎn)生哈希碰撞。若 key 值內(nèi)容相同則替換舊的 value,不然連接到鏈表后面,鏈表長(zhǎng)度超過(guò)閾值 8 就轉(zhuǎn)換為紅黑樹(shù)存儲(chǔ)。
3.什么是哈希碰撞,如何解決哈希碰撞?
答:只要兩個(gè)元素的 key 計(jì)算的哈希碼值相同就會(huì)發(fā)生哈希碰撞。jdk8 之前使用鏈表解決哈希碰撞。jdk8之后使用鏈表 + 紅黑樹(shù)解決哈希碰撞。
4.如果兩個(gè)鍵的 hashCode 相同,如何存儲(chǔ)鍵值對(duì)?
答:通過(guò) equals 比較內(nèi)容是否相同。
- 相同:則新的 value 覆蓋之前的 value。
- 不相同:遍歷該桶位的鏈表(或者樹(shù)):
- 如果找到相同key,則覆蓋該key對(duì)應(yīng)的value;
- 如果找不到,則將新的鍵值對(duì)添加到鏈表(或者樹(shù))中;
3.HashMap繼承體系
從繼承體系可以看出:
- HashMap 實(shí)現(xiàn)了Cloneable接口,可以被克隆。
- HashMap 實(shí)現(xiàn)了Serializable接口,屬于標(biāo)記性接口,HashMap 對(duì)象可以被序列化和反序列化。
- HashMap 繼承了AbstractMap,父類(lèi)提供了 Map 實(shí)現(xiàn)接口,具有Map的所有功能,以最大限度地減少實(shí)現(xiàn)此接口所需的工作。
知識(shí)擴(kuò)展:
通過(guò)上述繼承關(guān)系我們發(fā)現(xiàn)一個(gè)很奇怪的現(xiàn)象,就是 HashMap 已經(jīng)繼承了AbstractMap 而 AbstractMap 類(lèi)實(shí)現(xiàn)了Map 接口,那為什么 HashMap 還要在實(shí)現(xiàn) Map 接口呢?同樣在 ArrayList 中 LinkedLis 中都是這種結(jié)構(gòu)。
據(jù) Java 集合框架的創(chuàng)始人 Josh Bloch 描述,這樣的寫(xiě)法是一個(gè)失誤。在 Java 集合框架中,類(lèi)似這樣的寫(xiě)法很多,最幵始寫(xiě) Java 集合框架的時(shí)候,他認(rèn)為這樣寫(xiě),在某些地方可能是有價(jià)值的,直到他意識(shí)到錯(cuò)了。顯然的,jdk 的維護(hù)者,后來(lái)不認(rèn)為這個(gè)小小的失誤值得去修改,所以就這樣保留下來(lái)了。
存儲(chǔ)結(jié)構(gòu)(再過(guò)一遍)
在Java中,HashMap的實(shí)現(xiàn)采用了(數(shù)組 + 鏈表 + 紅黑樹(shù))的復(fù)雜結(jié)構(gòu),數(shù)組的一個(gè)元素又稱(chēng)作桶。
在添加元素時(shí),會(huì)根據(jù)hash值算出元素在數(shù)組中的位置,如果該位置沒(méi)有元素,則直接把元素放置在此處,如果該位置有元素了,則把元素以鏈表的形式放置在鏈表的尾部。
當(dāng)一個(gè)鏈表的元素個(gè)數(shù)達(dá)到一定的數(shù)量(且數(shù)組的長(zhǎng)度達(dá)到一定的長(zhǎng)度)后,則把鏈表轉(zhuǎn)化為紅黑樹(shù),從而提高效率。
數(shù)組的查詢(xún)效率為O(1),鏈表的查詢(xún)效率是O(k),紅黑樹(shù)的查詢(xún)效率是O(log k),k為桶中的元素個(gè)數(shù),所以當(dāng)元素?cái)?shù)量非常多的時(shí)候,轉(zhuǎn)化為紅黑樹(shù)能極大地提高效率。
4.HashMap基本屬性與常量
/* * 序列化版本號(hào) */ private static final long serialVersionUID = 362498820763181265L; /** * HashMap的初始化容量(必須是 2 的 n 次冪)默認(rèn)的初始容量為16 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; /** * 最大的容量為2的30次方 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * 默認(rèn)的裝載因子 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 樹(shù)化閾值,當(dāng)一個(gè)桶中的元素個(gè)數(shù)大于等于8時(shí)進(jìn)行樹(shù)化 */ static final int TREEIFY_THRESHOLD = 8; /** * 樹(shù)降級(jí)為鏈表的閾值,當(dāng)一個(gè)桶中的元素個(gè)數(shù)小于等于6時(shí)把樹(shù)轉(zhuǎn)化為鏈表 */ static final int UNTREEIFY_THRESHOLD = 6; /** * 當(dāng)桶的個(gè)數(shù)達(dá)到64的時(shí)候才進(jìn)行樹(shù)化 */ static final int MIN_TREEIFY_CAPACITY = 64; /** * Node數(shù)組,又叫作桶(bucket) */ transient Node<K,V>[] table; /** * 作為entrySet()的緩存 */ transient Set<Map.Entry<K,V>> entrySet; /** * 元素的數(shù)量 */ transient int size; /** * 修改次數(shù),用于在迭代的時(shí)候執(zhí)行快速失敗策略 */ transient int modCount; /** * 當(dāng)桶的使用數(shù)量達(dá)到多少時(shí)進(jìn)行擴(kuò)容,threshold = capacity * loadFactor */ int threshold; /** * 裝載因子 */ final float loadFactor;
(1)容量:容量為數(shù)組的長(zhǎng)度,亦即桶的個(gè)數(shù),默認(rèn)為16 ,最大為2的30次方,當(dāng)容量達(dá)到64時(shí)才可以樹(shù)化。
(2)裝載因子:裝載因子用來(lái)計(jì)算容量達(dá)到多少時(shí)才進(jìn)行擴(kuò)容,默認(rèn)裝載因子為0.75。
(3)樹(shù)化:樹(shù)化,當(dāng)容量達(dá)到64且鏈表的長(zhǎng)度達(dá)到8時(shí)進(jìn)行樹(shù)化,當(dāng)鏈表的長(zhǎng)度小于6時(shí)反樹(shù)化。
4.1 DEFAULT_INITIAL_CAPACITY
集合的初始化容量(必須是 2 的 n 次冪):
// 默認(rèn)的初始容量是16 1 << 4 相當(dāng)于 1*2的4次方 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
面試問(wèn)題:為什么必須是 2 的 n 次冪?如果輸入值不是 2 的冪比如 10 會(huì)怎么樣?
HashMap 構(gòu)造方法可以指定集合的初始化容量大小,如:
// 構(gòu)造一個(gè)帶指定初始容量和默認(rèn)負(fù)載因子(0.75)的空 HashMap。 HashMap(int initialCapacity)
根據(jù)上述講解我們已經(jīng)知道,當(dāng)向 HashMap 中添加一個(gè)元素的時(shí)候,需要根據(jù) key 的 hash 值,去確定其在數(shù)組中的具體位置。HashMap 為了存取高效,減少碰撞,就是要盡量把數(shù)據(jù)分配均勻,每個(gè)鏈表長(zhǎng)度大致相同,這個(gè)實(shí)現(xiàn)的關(guān)鍵就在把數(shù)據(jù)存到哪個(gè)鏈表中的算法。
這個(gè)算法實(shí)際就是取模,hash % length,而計(jì)算機(jī)中直接求余效率不如位移運(yùn)算。所以源碼中做了優(yōu)化,使用 hash & (length - 1),而實(shí)際上 hash % length 等于 hash & ( length - 1) 的前提是 length 是 2 的 n 次冪。(這段話(huà)是摘抄傳智播客鎖哥的,這個(gè)解釋確實(shí)很完美!)
例如,數(shù)組長(zhǎng)度為 8 的時(shí)候,3 & (8 - 1) = 3,2 & (8 - 1) = 2,桶的位置是(數(shù)組索引)3和2,不同位置上,不碰撞。
再來(lái)看一個(gè)數(shù)組長(zhǎng)度(桶位數(shù))不是2的n次冪的情況:
從上圖可以看出,當(dāng)數(shù)組長(zhǎng)度為9(非2 的n次冪)的時(shí)候,不同的哈希值hash, hash & (length - 1)所得到的數(shù)組下標(biāo)相等(很容易出現(xiàn)哈希碰撞)。
小結(jié)一下HashMap數(shù)組容量使用2的n次冪的原因:(面試也會(huì)問(wèn))
問(wèn)題:如果創(chuàng)建HashMap對(duì)象時(shí),輸入的數(shù)組長(zhǎng)度length是10,而不是2的n次冪會(huì)怎么樣呢?
HashMap<String, Integer> hashMap = new HashMap(10);
HashMap雙參構(gòu)造函數(shù)會(huì)通過(guò)tableSizeFor(initialCapacity)方法,得到一個(gè)最接近length且大于length的2的n次冪數(shù)(比如最接近10且大于10的2的n次冪數(shù)是16)
這一塊兒比較難理解,下文講構(gòu)造方法的時(shí)候還會(huì)再舉例一個(gè)例子:
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; }
說(shuō)明:
當(dāng)在實(shí)例化 HashMap 實(shí)例時(shí),如果給定了 initialCapacity,由于 HashMap 的 capacity 必須是 2 的冪,因此這個(gè)方法tableSizeFor(initialCapacity);用于找到大于等于 initialCapacity 的最小的 2 的冪。
分析:
int n = cap - 1;為什么要減去1呢?
防止 cap 已經(jīng)是 2 的冪。如果 cap 已經(jīng)是 2 的冪,又沒(méi)有這個(gè)減 1 操作,則執(zhí)行完后面的幾條無(wú)符號(hào)操作之后,返回的 capacity 將是這個(gè) cap 的 2 倍(后面還會(huì)再舉個(gè)例子講這個(gè))。
最后為什么有個(gè) n + 1 的操作呢?
如果 n 這時(shí)為 0 了(經(jīng)過(guò)了cap - 1后),則經(jīng)過(guò)后面的幾次無(wú)符號(hào)右移依然是 0,返回0是肯定不行的,所以最后返回n+1最終得到的 capacity 是1。
注意:容量最大也就是 32bit 的正數(shù),因此最后 n |= n >>> 16;最多也就 32 個(gè) 1(但是這已經(jīng)是負(fù)數(shù)了,在執(zhí)行 tableSizeFor 之前,對(duì) initialCapacity 做了判斷,如果大于MAXIMUM_CAPACITY(2 ^ 30),則取 MAXIMUM_CAPACITY。如果等于MAXIMUM_CAPACITY,會(huì)執(zhí)行位移操作。所以這里面的位移操作之后,最大 30 個(gè) 1,不會(huì)大于等于 MAXIMUM_CAPACITY。30 個(gè) 1,加 1 后得 2 ^ 30)。
完整例子:
所以由結(jié)果可得,當(dāng)執(zhí)行完tableSizeFor(initialCapacity);方法后,得到的新capacity是最接近initialCapacity且大于initialCapacity的2的n次冪的數(shù)。
4.2 DEFAULT_LOAD_FACTOR
默認(rèn)的負(fù)載因子(默認(rèn)值 0.75)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
4.3 MAXIMUM_CAPACITY
集合最大容量
static final int MAXIMUM_CAPACITY = 1 << 30; // 2的30次冪
4.4 TREEIFY_THRESHOLD
當(dāng)鏈表的值超過(guò)8則會(huì)轉(zhuǎn)為紅黑樹(shù)(jdk1.8新增)
// 當(dāng)桶(bucket)上的結(jié)點(diǎn)數(shù)大于這個(gè)值時(shí)會(huì)轉(zhuǎn)為紅黑樹(shù) static final int TREEIFY_THRESHOLD = 8;
面試問(wèn)題:為什么 Map 桶中結(jié)點(diǎn)個(gè)數(shù)超過(guò) 8 才轉(zhuǎn)為紅黑樹(shù)?
8這個(gè)閾值定義在HashMap中,針對(duì)這個(gè)成員變量,在源碼的注釋中只說(shuō)明了 8 是 bin( bucket 桶)從鏈表轉(zhuǎn)成樹(shù)的閾值,但是并沒(méi)有說(shuō)明為什么是 8。
在 HashMap 中有一段注釋說(shuō)明:
Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). And when they become too small (due to removal or resizing) they are converted back to plain bins. In usages with well-distributed user hashCodes, tree bins are rarely used. Ideally, under random hashCodes, the frequency of nodes in bins follows a Poisson distribution (http://en.wikipedia.org/wiki/Poisson_distribution) with a parameter of about 0.5 on average for the default resizing threshold of 0.75, although with a large variance because of resizing granularity. Ignoring variance, the expected occurrences of list size k are (exp(-0.5) * pow(0.5, k) / factorial(k)). The first values are: 翻譯:因?yàn)闃?shù)結(jié)點(diǎn)的大小大約是普通結(jié)點(diǎn)的兩倍,所以我們只在箱子包含足夠的結(jié)點(diǎn)時(shí)才使用樹(shù)結(jié)點(diǎn)(參見(jiàn)TREEIFY_THRESHOLD)。 當(dāng)它們變得太?。ㄓ捎趧h除或調(diào)整大小)時(shí),就會(huì)被轉(zhuǎn)換回普通的桶。在使用分布良好的用戶(hù) hashCode 時(shí),很少使用樹(shù)箱。 理想情況下,在隨機(jī)哈希碼下,箱子中結(jié)點(diǎn)的頻率服從泊松分布。 默認(rèn)調(diào)整閾值為0.75,平均參數(shù)約為0.5,盡管由于調(diào)整粒度的差異很大。忽略方差,列表大小k的預(yù)朗出現(xiàn)次數(shù)是(exp(-0.5)*pow(0.5, k) / factorial(k) 第一個(gè)值是: 0: 0.60653066 1: 0.30326533 2: 0.07581633 3: 0.01263606 4: 0.00157952 5: 0.00015795 6: 0.00001316 7: 0.00000094 8: 0.00000006 more: less than 1 in ten million
TreeNodes(樹(shù)) 占用空間是普通 Nodes(鏈表) 的兩倍,所以只有當(dāng) bin(bucket 桶) 包含足夠多的結(jié)點(diǎn)時(shí)才會(huì)轉(zhuǎn)成 TreeNodes,而是否足夠多就是由 TREEIFY_THRESH〇LD 的值決定的。當(dāng) bin(bucket 桶) 中結(jié)點(diǎn)數(shù)變少時(shí),又會(huì)轉(zhuǎn)成普通的 bin(bucket 桶)。并且我們查看源碼的時(shí)候發(fā)現(xiàn),鏈表長(zhǎng)度達(dá)到 8 就轉(zhuǎn)成紅黑樹(shù),當(dāng)長(zhǎng)度降到 6 就轉(zhuǎn)成普通 bin(bucket 桶)。
這樣就解釋了為什么不是一開(kāi)始就將其轉(zhuǎn)換為 TreeNodes,而是需要一定結(jié)點(diǎn)數(shù)之后才轉(zhuǎn)為 TreeNodes,說(shuō)白了就是權(quán)衡空間和時(shí)間。
這段內(nèi)容還說(shuō)到:當(dāng) hashCode 離散性很好的時(shí)候,樹(shù)型 bin 用到的概率非常小,因?yàn)閿?shù)據(jù)均勻分布在每個(gè) bin 中,幾乎不會(huì)有 bin 中鏈表長(zhǎng)度會(huì)達(dá)到閾值。但是在隨機(jī) hashCode 下,離散性可能會(huì)變差,然而 jdk 又不能阻止用戶(hù)實(shí)現(xiàn)這種不好的 hash 算法,因此就可能導(dǎo)致不均勻的數(shù)據(jù)分布。不理想情況下隨機(jī) hashCode 算法下所有 bin 中結(jié)點(diǎn)的分布頻率會(huì)遵循泊松分布,我們可以看到,一個(gè) bin 中鏈表長(zhǎng)度達(dá)到 8 個(gè)元素的槪率為 0.00000006,幾乎是不可能事件。所以,之所以選擇 8,不是隨便決定的,而是裉據(jù)概率統(tǒng)計(jì)決定的。甶此可見(jiàn),發(fā)展將近30年的 Java 每一項(xiàng)改動(dòng)和優(yōu)化都是非常嚴(yán)謹(jǐn)和科學(xué)的。
面試答案:hashCode 算法下所有 桶 中結(jié)點(diǎn)的分布頻率會(huì)遵循泊松分布,這時(shí)一個(gè)桶中鏈表長(zhǎng)度超過(guò) 8 個(gè)元素的槪率非常小,權(quán)衡空間和時(shí)間復(fù)雜度,所以選擇 8 這個(gè)數(shù)宇。
擴(kuò)展補(bǔ)充:
Poisson 分布(泊松分布),是一種統(tǒng)計(jì)與概率學(xué)里常見(jiàn)到的離散[概率分布]。泊松分布的概率函數(shù)為:
泊松分布的參數(shù) A 是單位時(shí)間(或單位面積)內(nèi)隨機(jī)事件的平均發(fā)生次數(shù)。泊松分布適合于描述單位時(shí)間內(nèi)隨機(jī)事件發(fā)生的次數(shù)。
以下是我在研究這個(gè)問(wèn)題時(shí),在一些資料上面翻看的解釋?zhuān)┐蠹覅⒖迹?/p>
紅黑樹(shù)的平均查找長(zhǎng)度是 log(n),如果長(zhǎng)度為 8,平均查找長(zhǎng)度為 log(8) = 3,鏈表的平均查找長(zhǎng)度為 n/2,當(dāng)長(zhǎng)度為 8 時(shí),平均查找長(zhǎng)虔為 8/2 = 4,這才有轉(zhuǎn)換成樹(shù)的必要;鏈表長(zhǎng)度如果是小于等于 6, 6/2 = 3,而 log(6) = 2.6,雖然速度也很快的,但是轉(zhuǎn)化為樹(shù)結(jié)構(gòu)和生成樹(shù)的時(shí)間并不會(huì)太短。
4.5 UNTREEIFY_THRESHOLD
當(dāng)鏈表的值小于 6 則會(huì)從紅黑樹(shù)轉(zhuǎn)回鏈表
// 當(dāng)桶(bucket)上的結(jié)點(diǎn)數(shù)小于這個(gè)值,樹(shù)轉(zhuǎn)為鏈表 static final int UNTREEIFY_THRESHOLD = 6;
4.6 MIN_TREEIFY_CAPACITY
當(dāng) Map 里面的數(shù)量超過(guò)這個(gè)值時(shí),表中的桶才能進(jìn)行樹(shù)形化,否則桶內(nèi)元素太多時(shí)會(huì)擴(kuò)容,而不是樹(shù)形化為了避免進(jìn)行擴(kuò)容、樹(shù)形化選擇的沖突,這個(gè)值不能小于4*TREEIFY_THRESHOLD(8)
// 桶中結(jié)構(gòu)轉(zhuǎn)化為紅黑樹(shù)對(duì)應(yīng)的數(shù)組長(zhǎng)度最小的值 static final int MIN_TREEIFY_CAPACITY = 64;
4.7 table(重點(diǎn))
table 用來(lái)初始化(必須是二的n次冪)
// 存儲(chǔ)元素的數(shù)組 transient Node<K,V>[] table;
在 jdk1.8 中我們了解到 HashMap 是由數(shù)組加鏈表加紅黑樹(shù)來(lái)組成的結(jié)構(gòu),其中 table 就是 HashMap 中的數(shù)組,jdk8 之前數(shù)組類(lèi)型是 Entry<K,V> 類(lèi)型。從 jdk1.8 之后是 Node<K,V> 類(lèi)型。只是換了個(gè)名字,都實(shí)現(xiàn)了一樣的接口:Map.Entry<K,V>。負(fù)責(zé)存儲(chǔ)鍵值對(duì)數(shù)據(jù)的。
4.8 entrySet
用來(lái)存放緩存
// 存放具體元素的集合 transient Set<Map.Entry<K,V>> entrySet;
4.9 size(重點(diǎn))
HashMap 中存放元素的個(gè)數(shù)
// 存放元素的個(gè)數(shù),注意這個(gè)不等于數(shù)組的長(zhǎng)度 transient int size;
size 為 HashMap 中 K-V 的實(shí)時(shí)數(shù)量,不是數(shù)組 table 的長(zhǎng)度。
4.10 modCount
用來(lái)記錄 HashMap 的修改次數(shù)
// 每次擴(kuò)容和更改 map 結(jié)構(gòu)的計(jì)數(shù)器 transient int modCount;
4.11 threshold(重點(diǎn))
用來(lái)調(diào)整大小下一個(gè)容量的值計(jì)算方式為(容量*負(fù)載因子)
// 臨界值 當(dāng)實(shí)際大?。ㄈ萘?負(fù)載因子)超過(guò)臨界值時(shí),會(huì)進(jìn)行擴(kuò)容 int threshold;
4.12 loadFactor(重點(diǎn))
哈希表的負(fù)載因子
// 負(fù)載因子 final float loadFactor;// 0.75f
說(shuō)明:
loadFactor 是用來(lái)衡量 HashMap 滿(mǎn)的程度,表示HashMap的疏密程度,影響 hash 操作到同一個(gè)數(shù)組位置的概率,計(jì)算 HashMap 的實(shí)時(shí)負(fù)載因子的方法為:size/capacity,而不是占用桶的數(shù)量去除以 capacity。capacity 是桶的數(shù)量,也就是 table 的長(zhǎng)度 length。loadFactor 太大導(dǎo)致查找元素效率低,太小導(dǎo)致數(shù)組的利用率低,存放的數(shù)據(jù)會(huì)很分散。loadFactor 的默認(rèn)值為 0.75f 是官方給出的一個(gè)比較好的臨界值。當(dāng) HashMap 里面容納的元素已經(jīng)達(dá)到 HashMap 數(shù)組長(zhǎng)度的 75% 時(shí),表示 HashMap 太擠了,需要擴(kuò)容,而擴(kuò)容這個(gè)過(guò)程涉及到 rehash、復(fù)制數(shù)據(jù)等操作,非常消耗性能。所以開(kāi)發(fā)中盡量減少擴(kuò)容的次數(shù),可以通過(guò)創(chuàng)建 HashMap 集合對(duì)象時(shí)指定初始容量來(lái)盡量避免。在 HashMap 的構(gòu)造器中可以定制 loadFactor。
// 構(gòu)造方法,構(gòu)造一個(gè)帶指定初始容量和負(fù)載因子的空HashMap HashMap(int initialCapacity, float loadFactor);
為什么負(fù)載因子loadFactor 設(shè)置為0.75,初始化臨界值threshold是12?
loadFactor 越趨近于1,那么 數(shù)組中存放的數(shù)據(jù)(entry)也就越多,也就越密,也就是會(huì)讓鏈表的長(zhǎng)度增加,loadFactor 越小,也就是趨近于0,數(shù)組中存放的數(shù)據(jù)(entry)也就越少,也就越稀疏。
如果希望鏈表盡可能少些,要提前擴(kuò)容。有的數(shù)組空間有可能一直沒(méi)有存儲(chǔ)數(shù)據(jù),負(fù)載因子盡可能小一些。
舉例:
例如:負(fù)載因子是0.4。 那么16*0.4--->6 如果數(shù)組中滿(mǎn)6個(gè)空間就擴(kuò)容會(huì)造成數(shù)組利用率太低了。
負(fù)載因子是0.9。 那么16*0.9--->14 那么這樣就會(huì)導(dǎo)致鏈表有點(diǎn)多了,導(dǎo)致查找元素效率低。
所以既兼顧數(shù)組利用率又考慮鏈表不要太多,經(jīng)過(guò)大量測(cè)試 0.75 是最佳方案。
threshold 計(jì)算公式:capacity(數(shù)組長(zhǎng)度默認(rèn)16) * loadFactor(負(fù)載因子默認(rèn)0.75)==12。
這個(gè)值是當(dāng)前已占用數(shù)組長(zhǎng)度的最大值。當(dāng) Size >= threshold(12) 的時(shí)候,那么就要考慮對(duì)數(shù)組的 resize(擴(kuò)容),也就是說(shuō),這個(gè)的意思就是 衡量數(shù)組是否需要擴(kuò)增的一個(gè)標(biāo)準(zhǔn)。 擴(kuò)容后的 HashMap 容量是之前容量的兩倍。
5.內(nèi)部類(lèi)
5.1Node內(nèi)部類(lèi)
Node是一個(gè)典型的單鏈表節(jié)點(diǎn),其中,hash用來(lái)存儲(chǔ)key計(jì)算得來(lái)的hash值。
static class Node<K,V> implements Map.Entry<K,V> { final int hash;// hash用來(lái)存儲(chǔ)key計(jì)算得來(lái)的hash值 final K key;// 鍵 V value;// 值 Node<K,V> next;// 下一個(gè)node節(jié)點(diǎn) 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() {// 調(diào)用底層c++ 返回Key/Value的哈希碼值,如果此對(duì)象為null,則返回0 return Objects.hashCode(key) ^ Objects.hashCode(value);// 將Key/Vaule } 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; } }
5.2TreeNode內(nèi)部類(lèi)
TreeNode內(nèi)部類(lèi),它繼承自L(fǎng)inkedHashMap中的Entry類(lèi),關(guān)于LInkedHashMap.Entry這個(gè)類(lèi)之后會(huì)單獨(dú)發(fā)文章論述,TreeNode是一個(gè)典型的樹(shù)型節(jié)點(diǎn),其中,prev是鏈表中的節(jié)點(diǎn),用于在刪除元素的時(shí)候可以快速找到它的前置節(jié)點(diǎn)。
// 位于HashMap中,文章接下來(lái)會(huì)逐步分析 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; } // 位于LinkedHashMap中,典型的雙向鏈表節(jié)點(diǎn),這個(gè)類(lèi)之后會(huì)單獨(dú)發(fā)文章論述 static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } }
6.HashMap構(gòu)造方法
HashMap 中重要的構(gòu)造方法,它們分別如下:
6.1 HashMap()
構(gòu)造一個(gè)空的HashMap,默認(rèn)初始容量(16)和默認(rèn)負(fù)載因子(0.75)。
public HashMap() { // 將默認(rèn)的負(fù)載因子0.75賦值給loadFactor,并沒(méi)有創(chuàng)建數(shù)組 this.loadFactor = DEFAULT_LOAD_FACTOR; }
6.2 HashMap(int initialCapacity)
構(gòu)造一個(gè)具有指定的初始容量和默認(rèn)負(fù)載因子(0.75)HashMap 。
// 指定“容量大小”的構(gòu)造函數(shù) public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
6.3 HashMap(int initialCapacity,float loadFactor)構(gòu)造方法
構(gòu)造一個(gè)具有指定的初始容量和負(fù)載因子的 HashMap。
/* 指定“容量大小”和“負(fù)載因子”的構(gòu)造函數(shù) initialCapacity:指定的容量 loadFactor:指定的負(fù)載因子 */ public HashMap(int initialCapacity, float loadFactor) { // 判斷初始化容量initialCapacity是否小于0 if (initialCapacity < 0) // 如果小于0,則拋出非法的參數(shù)異常IllegalArgumentException throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); // 判斷初始化容量initialCapacity是否大于集合的最大容量MAXIMUM_CAPACITY if (initialCapacity > MAXIMUM_CAPACITY) // 如果超過(guò)MAXIMUM_CAPACITY,會(huì)將MAXIMUM_CAPACITY賦值給initialCapacity initialCapacity = MAXIMUM_CAPACITY; // 判斷負(fù)載因子loadFactor是否小于等于0或者是否是一個(gè)非數(shù)值 if (loadFactor <= 0 || Float.isNaN(loadFactor)) // 如果滿(mǎn)足上述其中之一,則拋出非法的參數(shù)異常IllegalArgumentException throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // 將指定的負(fù)載因子賦值給HashMap成員變量的負(fù)載因子loadFactor this.loadFactor = loadFactor;// 一般不建議修改默認(rèn)的負(fù)載因子 this.threshold = tableSizeFor(initialCapacity); } // 最后調(diào)用了tableSizeFor,來(lái)看一下方法實(shí)現(xiàn): /* 返回比指定cap容量大的最小2的n次冪數(shù): 前面第一遍講述的應(yīng)該有些小伙伴難以理解,這里我在舉例解析一下: ------------------------------------------------------- 首先假定傳入的cap = 10 則,n = 10 -1 => 9 n |= n >>> 1 就等同于 n = (n | n >>> 1),所以: (位運(yùn)算不懂的可以去看我的《Java基礎(chǔ)提高之位運(yùn)算》這篇文章) 9 => 0b1001 9 >>> 1 => 0b0100 n |= n >>> 1; ===> 0b1001 | 0b0100 => 0b1101 n |= n >>> 2; ===> 0b1101 | 0b0011 => 0b1111 n |= n >>> 4; ===> 0b1111 | 0b0000 => 0b1111 n |= n >>> 8; ===> 0b1111 | 0b0000 => 0b1111 n |= n >>> 16; ===> 0b1111 | 0b0000 => 0b1111 得到: 0b1111 => 15 返回: return 15 + 1 => 16 ------------------------------------------------------- 如果cap 不減去1,即直接使n等于cap的話(huà),int n = cap; 我們繼續(xù)用上邊返回的cap => 16 傳入tableSizeFor(int cap): cap = 16 n = 16 16 => 0b10000 16 >>> 1 => 0b01000 n |= n >>> 1; ===> 0b10000 | 0b01000 => 0b11000 n |= n >>> 2; ===> 0b11000 | 0b00110 => 0b11110 n |= n >>> 4; ===> 0b11110 | 0b00001 => 0b11111 n |= n >>> 8; ===> 0b11111 | 0b00000 => 0b11111 n |= n >>> 16; ===> 0b11111 | 0b00000 => 0b11111 得到: 0b11111 => 31 返回 return 31 +1 => 32 而實(shí)際情況是應(yīng)該傳入cap = 16 , n = cap -1 = 15 15 => 0b1111 n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; 經(jīng)過(guò)上面運(yùn)算后得到:還是15 返回結(jié)果: return 15 + 1 = 16 所以我們得出結(jié)果: 防止 cap 已經(jīng)是 2 的冪數(shù)情況下。沒(méi)有這個(gè)減 1 操作, 則執(zhí)行完幾條無(wú)符號(hào)位移或位運(yùn)算操作之后,返回的cap(32)將是實(shí)際所需cap(16)的 2倍。 */ 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; }
說(shuō)明:
對(duì)于this.threshold = tableSizeFor(initialCapacity); 疑問(wèn)?
**tableSizeFor(initialCapacity)**判斷指定的初始化容量是否是2的n次冪,如果不是那么會(huì)變?yōu)楸戎付ǔ跏蓟萘看蟮淖钚〉?的n次冪。
但是注意,在tableSizeFor方法體內(nèi)部將計(jì)算后的數(shù)據(jù)返回給調(diào)用這里了,并且直接賦值給threshold邊界值了。有些人會(huì)覺(jué)得這里是一個(gè)bug,應(yīng)該這樣書(shū)寫(xiě):
this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;
這樣才符合threshold的意思(當(dāng)HashMap的size到達(dá)threshold這個(gè)閾值時(shí)會(huì)擴(kuò)容)
但是請(qǐng)注意,在jdk8以后的構(gòu)造方法中,并沒(méi)有對(duì)table這個(gè)成員變量進(jìn)行初始化,table的初始化被推遲到了put方法中,在put方法中會(huì)對(duì)threshold重新計(jì)算。
6.4 HashMap(Map<? extends K, ? extends V> m)
包含另一個(gè) “Map” 的構(gòu)造函數(shù)
// 構(gòu)造一個(gè)映射關(guān)系與指定 Map 相同的新 HashMap。 public HashMap(Map<? extends K, ? extends V> m) { // 負(fù)載因子loadFactor變?yōu)槟J(rèn)的負(fù)載因子0.75 this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
最后調(diào)用了 putMapEntries(),來(lái)看一下方法實(shí)現(xiàn):
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { //獲取參數(shù)集合的長(zhǎng)度 int s = m.size(); if (s > 0) {//判斷參數(shù)集合的長(zhǎng)度是否大于0 if (table == null) { // 判斷table是否已經(jīng)初始化 // 未初始化,s為m的實(shí)際元素個(gè)數(shù) float ft = ((float)s / loadFactor) + 1.0F;// 得到新的擴(kuò)容閾值 int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);// 新的擴(kuò)容閾值float自動(dòng)向下轉(zhuǎn)型為int // 計(jì)算得到的t大于閾值,則初始化閾值,將其變?yōu)榉弦蟮?的n次冪數(shù) if (t > threshold) threshold = tableSizeFor(t); } // 如果table已初始化過(guò)了,并且m元素個(gè)數(shù)大于閾值,進(jìn)行擴(kuò)容處理 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(); // 得到的key 和 value 放入 hashmap putVal(hash(key), key, value, false, evict); } } }
(小結(jié)):
面試問(wèn)題:float ft = ((float)s / loadFactor) + 1.0F; 這一行代碼中為什么要加 1.0F ?
(float)s/loadFactor 的結(jié)果是小數(shù),加 1.0F 與 (int)ft 相當(dāng)于是對(duì)小數(shù)做一個(gè)向上取整以盡可能的保證更大容量,更大的容量能夠減少 resize 的調(diào)用次數(shù)(為了效率,應(yīng)當(dāng)盡量減少擴(kuò)容的次數(shù))。所以 + 1.0F 是為了獲取更大的容量。
例如:原來(lái)集合的元素個(gè)數(shù)是 6 個(gè),那么 6/0.75 是8,由于8是 2 的n次冪,那么
if (t > threshold) threshold = tableSizeFor(t);執(zhí)行過(guò)后,新的數(shù)組大小就是 8 了。然后原來(lái)數(shù)組的數(shù)據(jù)就會(huì)存儲(chǔ)到長(zhǎng)度是 8 的新的數(shù)組中了,這樣會(huì)導(dǎo)致在存儲(chǔ)元素的時(shí)候,容量不夠,還得繼續(xù)擴(kuò)容,那么性能降低了,而如果 +1 呢,數(shù)組長(zhǎng)度直接變?yōu)?6了,這樣可以減少數(shù)組的擴(kuò)容。
7.HashMap的成員方法
7.1 put(K key, V value)方法
put方法是比較復(fù)雜的,實(shí)現(xiàn)步驟大致如下:
1.先通過(guò) hash 值計(jì)算出 key 映射到哪個(gè)桶;
2.如果桶上沒(méi)有碰撞沖突,則直接插入;
3.如果出現(xiàn)碰撞沖突了,則需要處理沖突:
a 如果該桶使用紅黑樹(shù)處理沖突,則調(diào)用紅黑樹(shù)的方法插入數(shù)據(jù);
b 否則采用傳統(tǒng)的鏈?zhǔn)椒椒ú迦?。如果鏈的長(zhǎng)度達(dá)到臨界值,則把鏈轉(zhuǎn)變?yōu)榧t黑樹(shù);
4.如果桶中存在重復(fù)的鍵,則為該鍵替換新值 value;
5.如果 size 大于閾值 threshold,則進(jìn)行擴(kuò)容;
具體的方法如下:
public V put(K key, V value) { // 調(diào)用hash(key)計(jì)算出key的hash值 return putVal(hash(key), key, value, false, true); }
說(shuō)明:
- HashMap 只提供了 put 用于添加元素,putVal 方法只是給 put 方法調(diào)用的一個(gè)方法,并沒(méi)有提供給用戶(hù)使用。 所以我們重點(diǎn)看 putVal 方法。
- 我們可以看到在 putVal 方法中 key 在這里執(zhí)行了一下 hash 方法,來(lái)看一下 hash 方法是如何實(shí)現(xiàn)的。
static final int hash(Object key) {
int h;
// 如果key為null,則hash值為0,
// 否則調(diào)用key的hashCode()方法計(jì)算出key的哈希值然后賦值給h,
// 后與h無(wú)符號(hào)右移16位后的二進(jìn)制進(jìn)行按位異或得到最后的hash值,
// 這樣做是為了使計(jì)算出的hash更分散
// 為什么要更分散呢?因?yàn)樵椒稚?,某個(gè)桶的鏈表長(zhǎng)度就越短,之后生成的紅黑樹(shù)越少,效率越高
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
從上面可以得知 HashMap 是支持 key 為空的,而 HashTable 是直接用 Key 來(lái)獲取hashCode 所以 key 為空會(huì)拋異常。
解讀上述 hash 方法:
我們先研究下 key 的哈希值是如何計(jì)算出來(lái)的。key 的哈希值是通過(guò)上述方法計(jì)算出來(lái)的。
這個(gè)哈希方法首先計(jì)算出 key 的 hashCode 賦值給 h,然后與 h 無(wú)符號(hào)右移 16 位后的二進(jìn)制進(jìn)行按位異或得到最后的 hash 值。
在 putVal 函數(shù)中使用到了上述 hash 函數(shù)計(jì)算的哈希值:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { ... if ((p = tab[i = (n - 1) & hash]) == null) // 這里的n表示數(shù)組長(zhǎng)度16 ,公式 // (length - 1) & hash = 桶位下標(biāo) 當(dāng)數(shù)組長(zhǎng)度為2的n次冪數(shù)時(shí), // 該公式相當(dāng)于:hash % length 哈希值對(duì)數(shù)組長(zhǎng)度取余 // 例如:hash % 32 = hash & (32-1) ... }
計(jì)算過(guò)程如下所示:
說(shuō)明:
- key.hashCode();返回散列值也就是 hashcode,假設(shè)隨便生成的一個(gè)值。
- n 表示數(shù)組初始化的長(zhǎng)度是 16。
- &(按位與運(yùn)算):運(yùn)算規(guī)則:相同的二進(jìn)制數(shù)位上,都是 1 的時(shí)候,結(jié)果為 1,否則為0。
- ^(按位異或運(yùn)算):運(yùn)算規(guī)則:相同的二進(jìn)制數(shù)位上,數(shù)字相同,結(jié)果為 0,不同為 1。
最后獲得0101==> 下標(biāo)為5的捅。
簡(jiǎn)單來(lái)說(shuō)就是:
高 16bit 不變,低 16bit 和高 16bit 做了一個(gè)異或(得到的 hashCode 轉(zhuǎn)化為 32 位二進(jìn)制,前 16 位和后 16 位低 16bit 和高 16bit 做了一個(gè)異或)。
問(wèn)題:為什么要這樣操作呢?
如果當(dāng) n 即數(shù)組長(zhǎng)度很小,假設(shè)是 16 的話(huà),那么 n - 1 即為 1111 ,這樣的值和 hashCode 直接做按位與操作,實(shí)際上只使用了哈希值的后 4 位。如果當(dāng)哈希值的高位變化很大,低位變化很小,這樣就很容易造成哈希沖突了,所以這里把高低位都利用起來(lái),從而解決了這個(gè)問(wèn)題。
下面,我們來(lái)看看 putVal 方法,看看它到底做了什么。
主要參數(shù):
- hash:key 的 hash 值
- key:原始
- keyvalue:要存放的值
- onlyIfAbsent:如果
- true :代表不更改現(xiàn)有的值
- evict:如果為
- false:表示
- table:為創(chuàng)建狀態(tài)
putVal 方法源代碼如下所示:
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } static final int hash(Object key) { int h;// 如果key是null 則hash值為0,否則調(diào)用key的hashCode()方法,并讓高16位參與整個(gè)hash異或,如果數(shù)組長(zhǎng)度比較小的情況下,讓高16位也參與尋址, // 尋址公式:(length - 1) & hash // 這樣可以使計(jì)算出的結(jié)果更分散,不容易產(chǎn)生哈希沖突 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; /* 1)transient Node<K,V>[] table; 表示存儲(chǔ)Map集合中元素的數(shù)組。 2)(tab = table) == null 表示將空的table賦值給tab,然后判斷tab是否等于null,第一次肯定是null。 3)(n = tab.length) == 0 表示將數(shù)組的長(zhǎng)度0賦值給n,然后判斷n是否等于0,n等于0,由于if判斷使用雙或,滿(mǎn)足一個(gè)即可,則執(zhí)行代碼 n = (tab = resize()).length; 進(jìn)行數(shù)組初始化,并將初始化好的數(shù)組長(zhǎng)度賦值給n。 4)執(zhí)行完n = (tab = resize()).length,數(shù)組tab每個(gè)空間都是null。 */ if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; /* 1)i = (n - 1) & hash 表示計(jì)算數(shù)組的索引賦值給i,即確定元素存放在哪個(gè)桶中。 2)p = tab[i = (n - 1) & hash]表示獲取計(jì)算出的位置的數(shù)據(jù)賦值給結(jié)點(diǎn)p。 3) (p = tab[i = (n - 1) & hash]) == null 判斷結(jié)點(diǎn)位置是否等于null,如果為null,則執(zhí)行代碼:tab[i] = newNode(hash, key, value, null);根據(jù)鍵值對(duì)創(chuàng)建新的結(jié)點(diǎn)放入該位置的桶中。 小結(jié):如果當(dāng)前桶沒(méi)有哈希碰撞沖突,則直接把鍵值對(duì)插入空間位置。 */ if ((p = tab[i = (n - 1) & hash]) == null) // 創(chuàng)建一個(gè)新的結(jié)點(diǎn)存入到桶中 tab[i] = newNode(hash, key, value, null); else { // 執(zhí)行else說(shuō)明tab[i]不等于null,表示這個(gè)位置已經(jīng)有值了 Node<K,V> e; K k; /* 比較桶中第一個(gè)元素(數(shù)組中的結(jié)點(diǎn))的hash值和key是否相等 1)p.hash == hash :p.hash表示原來(lái)存在數(shù)據(jù)的hash值 hash表示后添加數(shù)據(jù)的hash值 比較兩個(gè)hash值是否相等。 說(shuō)明:p表示tab[i],即 newNode(hash, key, value, null)方法返回的Node對(duì)象。 Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) { return new Node<>(hash, key, value, next); } 而在Node類(lèi)中具有成員變量hash用來(lái)記錄著之前數(shù)據(jù)的hash值的。 2)(k = p.key) == key :p.key獲取原來(lái)數(shù)據(jù)的key賦值給k key 表示后添加數(shù)據(jù)的key比較兩個(gè)key的地址值是否相等。 3)key != null && key.equals(k):能夠執(zhí)行到這里說(shuō)明兩個(gè)key的地址值不相等,那么先判斷后添加的key是否等于null,如果不等于null再調(diào)用equals方法判斷兩個(gè)key的內(nèi)容是否相等。 */ if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) /* 說(shuō)明:兩個(gè)元素哈希值相等,并且key的值也相等, 將舊的元素整體對(duì)象賦值給e,用e來(lái)記錄 */ e = p; // hash值不相等或者key不相等;判斷p是否為紅黑樹(shù)結(jié)點(diǎn) else if (p instanceof TreeNode) // 放入樹(shù)中 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 說(shuō)明是鏈表結(jié)點(diǎn) else { /* 1)如果是鏈表的話(huà)需要遍歷到最后結(jié)點(diǎn)然后插入 2)采用循環(huán)遍歷的方式,判斷鏈表中是否有重復(fù)的key */ for (int binCount = 0; ; ++binCount) { /* 1)e = p.next 獲取p的下一個(gè)元素賦值給e。 2)(e = p.next) == null 判斷p.next是否等于null,等于null,說(shuō)明p沒(méi)有下一個(gè)元素,那么此時(shí)到達(dá)了鏈表的尾部,還沒(méi)有找到重復(fù)的key,則說(shuō)明HashMap沒(méi)有包含該鍵,將該鍵值對(duì)插入鏈表中。 */ if ((e = p.next) == null) { /* 1)創(chuàng)建一個(gè)新的結(jié)點(diǎn)插入到尾部 p.next = newNode(hash, key, value, null); Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) { return new Node<>(hash, key, value, next); } 注意第四個(gè)參數(shù)next是null,因?yàn)楫?dāng)前元素插入到鏈表末尾了,那么下一個(gè)結(jié)點(diǎn)肯定是null。 2)這種添加方式也滿(mǎn)足鏈表數(shù)據(jù)結(jié)構(gòu)的特點(diǎn),每次向后添加新的元素。 */ p.next = newNode(hash, key, value, null); /* 1)結(jié)點(diǎn)添加完成之后判斷此時(shí)結(jié)點(diǎn)個(gè)數(shù)是否大于TREEIFY_THRESHOLD臨界值8,如果大于則將鏈表轉(zhuǎn)換為紅黑樹(shù)。 2)int binCount = 0 :表示for循環(huán)的初始化值。從0開(kāi)始計(jì)數(shù)。記錄著遍歷結(jié)點(diǎn)的個(gè)數(shù)。值是0表示第一個(gè)結(jié)點(diǎn),1表示第二個(gè)結(jié)點(diǎn)。。。。7表示第八個(gè)結(jié)點(diǎn),加上數(shù)組中的的一個(gè)元素,元素個(gè)數(shù)是9。 TREEIFY_THRESHOLD - 1 --》8 - 1 ---》7 如果binCount的值是7(加上數(shù)組中的的一個(gè)元素,元素個(gè)數(shù)是9) TREEIFY_THRESHOLD - 1也是7,此時(shí)轉(zhuǎn)換紅黑樹(shù)。 */ if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st // 轉(zhuǎn)換為紅黑樹(shù) treeifyBin(tab, hash); // 跳出循環(huán) break; } /* 執(zhí)行到這里說(shuō)明e = p.next 不是null,不是最后一個(gè)元素。繼續(xù)判斷鏈表中結(jié)點(diǎn)的key值與插入的元素的key值是否相等。 */ if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 相等,跳出循環(huán) /* 要添加的元素和鏈表中的存在的元素的key相等了,則跳出for循環(huán)。不用再繼續(xù)比較了 直接執(zhí)行下面的if語(yǔ)句去替換去 if (e != null) */ break; /* 說(shuō)明新添加的元素和當(dāng)前結(jié)點(diǎn)不相等,繼續(xù)查找下一個(gè)結(jié)點(diǎn)。 用于遍歷桶中的鏈表,與前面的e = p.next組合,可以遍歷鏈表 */ p = e; } } /* 表示在桶中找到key值、hash值與插入元素相等的結(jié)點(diǎn) 也就是說(shuō)通過(guò)上面的操作找到了重復(fù)的鍵,所以這里就是把該鍵的值變?yōu)樾碌闹担⒎祷嘏f值 這里完成了put方法的修改功能 */ if (e != null) { // 記錄e的value V oldValue = e.value; // onlyIfAbsent為false或者舊值為null if (!onlyIfAbsent || oldValue == null) // 用新值替換舊值 // e.value 表示舊值 value表示新值 e.value = value; // 訪(fǎng)問(wèn)后回調(diào) afterNodeAccess(e); // 返回舊值 return oldValue; } } // 修改記錄次數(shù) ++modCount; // 判斷實(shí)際大小是否大于threshold閾值,如果超過(guò)則擴(kuò)容 if (++size > threshold) resize(); // 插入后回調(diào) afterNodeInsertion(evict); return null; }
(1)計(jì)算key的hash值;
(2)如果桶(數(shù)組)數(shù)量為0,則初始化桶;
(3)如果key所在的桶沒(méi)有元素,則直接插入;
(4)如果key所在的桶中的第一個(gè)元素的key與待插入的key相同,說(shuō)明找到了元素,轉(zhuǎn)后續(xù)流程(9)處理;
(5)如果第一個(gè)元素是樹(shù)節(jié)點(diǎn),則調(diào)用樹(shù)節(jié)點(diǎn)的putTreeVal()尋找元素或插入樹(shù)節(jié)點(diǎn);
(6)如果不是以上三種情況,則遍歷桶對(duì)應(yīng)的鏈表查找key是否存在于鏈表中;
(7)如果找到了對(duì)應(yīng)key的元素,則轉(zhuǎn)后續(xù)流程(9)處理;
(8)如果沒(méi)找到對(duì)應(yīng)key的元素,則在鏈表最后插入一個(gè)新節(jié)點(diǎn)并判斷是否需要樹(shù)化;
(9)如果找到了對(duì)應(yīng)key的元素,則判斷是否需要替換舊值,并直接返回舊值;
(10)如果插入了元素,則數(shù)量加1并判斷是否需要擴(kuò)容;
7.2 擴(kuò)容方法 resize()
擴(kuò)容機(jī)制:
1.什么時(shí)候才需要擴(kuò)容?
當(dāng) HashMap 中的元素個(gè)數(shù)超過(guò)數(shù)組大小(數(shù)組長(zhǎng)度)*loadFactor(負(fù)載因子)時(shí),就會(huì)進(jìn)行數(shù)組擴(kuò)容,loadFactor 的默認(rèn)值是 0.75。
2.HashMap 的擴(kuò)容是什么?
進(jìn)行擴(kuò)容,會(huì)伴隨著一次重新 hash 分配,并且會(huì)遍歷 hash 表中所有的元素,是非常耗時(shí)的。在編寫(xiě)程序中,要盡量避免 resize。
HashMap 在進(jìn)行擴(kuò)容時(shí),使用的 rehash 方式非常巧妙,因?yàn)槊看螖U(kuò)容都是翻倍,與原來(lái)計(jì)算的 (n - 1) & hash 的結(jié)果相比,只是多了一個(gè) bit 位,所以結(jié)點(diǎn)要么就在原來(lái)的位置,要么就被分配到 “原位置 + 舊容量” 這個(gè)位置。
例如我們從 16 擴(kuò)展為 32 時(shí),具體的變化如下所示:
因此元素在重新計(jì)算 hash 之后,因?yàn)?n 變?yōu)?2 倍,那么 n - 1 的標(biāo)記范圍在高位多 1bit(紅色),因此新的 index 就會(huì)發(fā)生這樣的變化。
說(shuō)明:
5 是假設(shè)計(jì)算出來(lái)的原來(lái)的索引。這樣就驗(yàn)證了上述所描述的:擴(kuò)容之后所以結(jié)點(diǎn)要么就在原來(lái)的位置,要么就被分配到 “原位置 + 舊容量” 這個(gè)位置。
因此,我們?cè)跀U(kuò)充 HashMap 的時(shí)候,不需要重新計(jì)算 hash,只需要看看原來(lái)的 hash 值新增的那個(gè) bit 是 1 還是 0 就可以了,是 0 的話(huà)索引沒(méi)變,是 1 的話(huà)索引變成 “原位置 + 舊容量” ??梢钥纯聪聢D為 16 擴(kuò)充為 32 的 resize 示意圖:
正是因?yàn)檫@樣巧妙的 rehash 方式,既省去了重新計(jì)算 hash 值的時(shí)間,而且同時(shí),由于新增的 1bit 是 0 還是 1 可以認(rèn)為是隨機(jī)的,在 resize 的過(guò)程中保證了 rehash 之后每個(gè)桶上的結(jié)點(diǎn)數(shù)一定小于等于原來(lái)桶上的結(jié)點(diǎn)數(shù),保證了 rehash 之后不會(huì)出現(xiàn)更嚴(yán)重的 hash 沖突,均勻的把之前的沖突的結(jié)點(diǎn)分散到新的桶中了。
源碼 resize 方法的解讀
下面是代碼的具體實(shí)現(xiàn):
/** * 為什么需要擴(kuò)容呢? * 為了解決哈希沖突導(dǎo)致的鏈化影響查詢(xún)效率問(wèn)題,擴(kuò)容會(huì)緩解該問(wèn)題 */ final Node<K,V>[] resize() { // oldTab:表示擴(kuò)容前的哈希表數(shù)組 Node<K,V>[] oldTab = table; // oldCap:表示擴(kuò)容之前table數(shù)組長(zhǎng)度 // 如果當(dāng)前哈希表數(shù)組等于null 長(zhǎng)度返回0,否則返回當(dāng)前哈希表數(shù)組的長(zhǎng)度 int oldCap = (oldTab == null) ? 0 : oldTab.length; // oldThr:表示擴(kuò)容之前的閥值(觸發(fā)本次擴(kuò)容的閾值) 默認(rèn)是12(16*0.75) int oldThr = threshold; // newCap:擴(kuò)容之后的table散列表數(shù)組長(zhǎng)度 // newThr: 擴(kuò)容之后,下次再出發(fā)擴(kuò)容的條件(新的擴(kuò)容閾值) int newCap, newThr = 0; // 如果老的哈希表數(shù)組長(zhǎng)度oldCap > 0 // 如果該條件成立,說(shuō)明hashMap 中的散列表數(shù)組已經(jīng)初始化過(guò)了,是一次正常擴(kuò)容 // 開(kāi)始計(jì)算擴(kuò)容后的大小 if (oldCap > 0) { // 擴(kuò)容之前的table數(shù)組大小已經(jīng)達(dá)到 最大閾值后,則不再擴(kuò)容 // 且設(shè)置擴(kuò)容條件為:int的最大值 if (oldCap >= MAXIMUM_CAPACITY) { // 修改閾值為int的最大值 threshold = Integer.MAX_VALUE; return oldTab; } // 擴(kuò)容之前的table數(shù)組大小沒(méi)超過(guò)最大值,則擴(kuò)充為原來(lái)的2倍 // (newCap = oldCap << 1) < MAXIMUM_CAPACITY 擴(kuò)大到2倍之后容量要小于最大容量 // oldCap >= DEFAULT_INITIAL_CAPACITY 原哈希表數(shù)組長(zhǎng)度大于等于數(shù)組初始化長(zhǎng)度16 // 如果oldCap 小于默認(rèn)初始容量16,比如傳入的默認(rèn)容量為8,則不執(zhí)行下面代碼 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) // 新的擴(kuò)容閾值擴(kuò)大一倍 newThr = oldThr << 1; // double threshold } // 如果老的哈希表數(shù)組長(zhǎng)度oldCap == 0 // 說(shuō)明hashMap中的散列表還沒(méi)有初始化,這時(shí)候是null // 如果老閾值oldThr大于0 直接賦值 /* 以下三種情況會(huì)直接進(jìn)入該判斷:(即,這時(shí)候oldThr擴(kuò)容閾值已存在) 1.new HashMap(initCap,loadFactor); 2.new HashMap(initCap); 3.new HashMap(Map);// 這個(gè)傳入的map中已經(jīng)有數(shù)據(jù) */ else if (oldThr > 0) // 老閾值賦值給新的數(shù)組長(zhǎng)度 newCap = oldThr; // 如果老的哈希表數(shù)組長(zhǎng)度oldCap == 0 // 說(shuō)明hashMap中的散列表還沒(méi)有初始化,這時(shí)候是null // 此時(shí),老擴(kuò)容閾值oldThr == 0 else { // 直接使用默認(rèn)值 newCap = DEFAULT_INITIAL_CAPACITY;//16 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//12 } // 如果執(zhí)行到這個(gè)位置新的擴(kuò)容閾值newThr還沒(méi)有得到賦值,則 // 需要計(jì)算新的resize最大上限 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } // 將新的閥值newThr賦值給threshold threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) // 創(chuàng)建新的散列表 // newCap是新的數(shù)組長(zhǎng)度---> 32 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; // 說(shuō)明:hashMap本次擴(kuò)容之前,table不為null if (oldTab != null) { // 把每個(gè)bucket桶的數(shù)據(jù)都移動(dòng)到新的散列表中 // 遍歷舊的哈希表的每個(gè)桶,重新計(jì)算桶里元素的新位置 for (int j = 0; j < oldCap; ++j) { // 當(dāng)前node節(jié)點(diǎn) Node<K,V> e; // 說(shuō)明:此時(shí)的當(dāng)前桶位中有數(shù)據(jù),但是數(shù)據(jù)具體是 // 1.單個(gè)數(shù)據(jù) 、 2.還是鏈表 、 3.還是紅黑樹(shù) 并不能確定 if ((e = oldTab[j]) != null) { // 原來(lái)的數(shù)據(jù)賦值為null 便于GC回收 oldTab[j] = null; // 第一種情況:判斷數(shù)組是否有下一個(gè)引用(是否是單個(gè)數(shù)據(jù)) if (e.next == null) // 沒(méi)有下一個(gè)引用,說(shuō)明不是鏈表, // 當(dāng)前桶上只有單個(gè)數(shù)據(jù)的鍵值對(duì), // 可以將數(shù)據(jù)直接放入新的散列表中 // e.hash & (newCap - 1) 尋址公式得到的索引結(jié)果有兩種: // 1.和原來(lái)舊散列表中的索引位置相同, // 2.原來(lái)舊散列表中的索引位置i + 舊容量oldCap newTab[e.hash & (newCap - 1)] = e; //第二種情況:桶位已經(jīng)形成紅黑樹(shù) else if (e instanceof TreeNode) // 說(shuō)明是紅黑樹(shù)來(lái)處理沖突的,則調(diào)用相關(guān)方法把樹(shù)分開(kāi) // 紅黑樹(shù)這塊,我會(huì)單獨(dú)寫(xiě)一篇博客給大家詳細(xì)分析一下 // 紅黑樹(shù)相關(guān)可以先跳過(guò) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 第三種情況:桶位已經(jīng)形成鏈表 else { // 采用鏈表處理沖突 // 低位鏈表: // 擴(kuò)容之后數(shù)組的下標(biāo)位置,與當(dāng)前數(shù)組的下標(biāo)位置一致 時(shí)使用 Node<K,V> loHead = null, loTail = null; // 高位鏈表:擴(kuò)容之后數(shù)組的下標(biāo)位置等于 // 當(dāng)前數(shù)組下標(biāo)位置 + 擴(kuò)容之前數(shù)組的長(zhǎng)度oldCap 時(shí)使用 Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; // 通過(guò)上述講解的原理來(lái)計(jì)算結(jié)點(diǎn)的新位置 do { // 原索引 next = e.next; // 這里來(lái)判斷如果等于true // e這個(gè)結(jié)點(diǎn)在resize之后不需要移動(dòng)位置 // 舉例: // 假如hash1 -> ...... 0 1111 // 假如oldCap=16 -> ...... 1 0000 // e.hash & oldCap 結(jié)果為0,則 // 擴(kuò)容之后數(shù)組的下標(biāo)位置j,與當(dāng)前數(shù)組的下標(biāo)位置一致 // 使用低位鏈表 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } // 舉例: // 假如hash2 -> ...... 1 1111 // 假如oldCap=16 -> ...... 1 0000 // e.hash & oldCap 結(jié)果不為0,則 // 擴(kuò)容之后數(shù)組的下標(biāo)位置為: // 當(dāng)前數(shù)組下標(biāo)位置j + 擴(kuò)容之前數(shù)組的長(zhǎng)度oldCap // 使用高位鏈表 else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 將低位鏈表放到bucket桶里 if (loTail != null) { loTail.next = null; // 索引位置=當(dāng)前數(shù)組下標(biāo)位置j newTab[j] = loHead; } // 將高位鏈表放到bucket里 if (hiTail != null) { hiTail.next = null; // 索引位置=當(dāng)前數(shù)組下標(biāo)位置j + 擴(kuò)容之前數(shù)組的長(zhǎng)度oldCap newTab[j + oldCap] = hiHead; } } } } } // 返回新散列表 return newTab; }
(1)如果使用是默認(rèn)構(gòu)造方法,則第一次插入元素時(shí)初始化為默認(rèn)值,容量為16,擴(kuò)容門(mén)檻為12;
(2)如果使用的是非默認(rèn)構(gòu)造方法,則第一次插入元素時(shí)初始化容量等于擴(kuò)容門(mén)檻,擴(kuò)容門(mén)檻在構(gòu)造方法里等于傳入容量向上最近的2的n次方;
(3)如果舊容量大于0,則新容量等于舊容量的2倍,但不超過(guò)最大容量2的30次方,新擴(kuò)容門(mén)檻為舊擴(kuò)容門(mén)檻的2倍;
(4)創(chuàng)建一個(gè)新容量的桶;
(5)搬移元素,原鏈表分化成兩個(gè)鏈表,低位鏈表存儲(chǔ)在原來(lái)桶的位置,高位鏈表搬移到原來(lái)桶的位置加舊容量的位置;
7.3 刪除方法 remove()
刪除方法就是首先先找到元素的位置,如果是鏈表就遍歷鏈表找到元素之后刪除。如果是用紅黑樹(shù)就遍歷樹(shù)然后找到之后做刪除,樹(shù)小于 6 的時(shí)候要轉(zhuǎn)鏈表。
刪除 remove() 方法:
// remove方法的具體實(shí)現(xiàn)在removeNode方法中,所以我們重點(diǎn)看下removeNode方法 // 根據(jù)key刪除 public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } // 根據(jù)key,value 刪除 @Override public boolean remove(Object key, Object value) { return removeNode(hash(key), key, value, true, true) != null; }
removeNode() 方法:
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { // 參數(shù): // matchValue 當(dāng)根據(jù) key和value 刪除的時(shí)候該參數(shù)為true // movable 可以先不用考慮這個(gè)參數(shù) // tab:引用當(dāng)前haashMap中的散列表 // p:當(dāng)前node元素 // n:當(dāng)前散列表數(shù)組長(zhǎng)度 // index:表示尋址結(jié)果 Node<K,V>[] tab; Node<K,V> p; int n, index; // 根據(jù)hash找到位置 // 如果當(dāng)前key映射到的桶不為空 if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { // 進(jìn)入這個(gè)if判斷內(nèi)部,說(shuō)明桶位是有數(shù)據(jù)的,需要進(jìn)行查詢(xún)操作,并且執(zhí)行刪除 // node:通過(guò)查找得到的要?jiǎng)h除的元素 // e:表示當(dāng)前node的下一個(gè)元素 // k,v 鍵 值 Node<K,V> node = null, e; K k; V v; // 第一種情況:當(dāng)前桶位中的元素 即為我們要?jiǎng)h除的元素 // 如果桶上的結(jié)點(diǎn)就是要找的key,則將node指向該結(jié)點(diǎn) if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; // 如果桶位中的頭一個(gè)元素不是我們要找的元素,且桶位中的e = p.next不為null // 說(shuō)明該桶位中的節(jié)點(diǎn)存在下一個(gè)節(jié)點(diǎn) else if ((e = p.next) != null) { // 說(shuō)明:當(dāng)前桶位,要么是 鏈表,要么是 紅黑樹(shù) // 第二種情況:判斷桶位中是否已經(jīng)形成了紅黑樹(shù) if (p instanceof TreeNode) // 說(shuō)明是以紅黑樹(shù)來(lái)處理的沖突,則獲取紅黑樹(shù)要?jiǎng)h除的結(jié)點(diǎn) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); // 第三種情況:桶位中已經(jīng)形成鏈表 else { // 判斷是否以鏈表方式處理hash沖突 // 是的話(huà)則通過(guò)遍歷鏈表來(lái)尋找要?jiǎng)h除的結(jié)點(diǎn) do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } // 比較找到的key的value和要?jiǎng)h除的是否匹配 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { // 第一種情況:如果桶位中是紅黑樹(shù),通過(guò)調(diào)用紅黑樹(shù)的方法來(lái)刪除結(jié)點(diǎn) if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); // 第二種情況:如果桶位中是鏈表 else if (node == p) // 鏈表刪除 tab[index] = node.next; // 如果桶位中 else // 第三種情況:將當(dāng)前元素p的下一個(gè)元素設(shè)置為 要?jiǎng)h除元素的 下一個(gè)元素 p.next = node.next; // 記錄修改次數(shù) ++modCount; // 變動(dòng)的數(shù)量 --size; afterNodeRemoval(node); return node; } } return null; }
7.4 查找元素方法 get()
查找方法,通過(guò)元素的 key 找到 value。
代碼如下:
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
get 方法主要調(diào)用的是 getNode 方法,代碼如下:
final Node<K,V> getNode(int hash, Object key) { // tab:引用當(dāng)前hashMap的散列表 // first:桶位中的頭元素 // e:臨時(shí)node元素 // n:table數(shù)組的長(zhǎng)度 Node<K,V>[] tab; Node<K,V> first, e; int n; K k; // 如果哈希表不為空,并且key對(duì)應(yīng)的桶不為空 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { /* 判斷數(shù)組元素是否相等 根據(jù)索引的位置檢查第一個(gè)元素 注意:總是檢查第一個(gè)元素 */ // 第一種情況:定位出來(lái)的桶位元素 就是我們要get的數(shù)據(jù) if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; // 桶位第一個(gè)元素不是我們要找的目標(biāo)元素,且first.next不為null // 說(shuō)明當(dāng)前桶位不止一個(gè)元素,可能是鏈表,也可能是紅黑樹(shù) if ((e = first.next) != null) { // 第二種情況:桶位已經(jīng)升級(jí)成了紅黑樹(shù) // 判斷是否是紅黑樹(shù),是的話(huà)調(diào)用紅黑樹(shù)中的getTreeNode方法獲取結(jié)點(diǎn) if (first instanceof TreeNode) // 調(diào)用與樹(shù)相關(guān)的方法得到目標(biāo)元素 return ((TreeNode<K,V>)first).getTreeNode(hash, key); // 第三種情況:桶位已經(jīng)形成鏈表 do { // 不是紅黑樹(shù)的話(huà),那就是鏈表結(jié)構(gòu)了 // 通過(guò)循環(huán)的方法判斷鏈表中是否存在該key if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } // 如果沒(méi)找到返回null return null; }
小結(jié):
get 方法實(shí)現(xiàn)的步驟:
a. 通過(guò) hash 值獲取該 key 映射到的桶
b. 桶上的 key 就是要查找的 key,則直接找到并返回
c. 桶上的 key 不是要找的 key,則查看后續(xù)的結(jié)點(diǎn):
如果后續(xù)結(jié)點(diǎn)是紅黑樹(shù)結(jié)點(diǎn),通過(guò)調(diào)用紅黑樹(shù)的方法根據(jù) key 獲取 value
如果后續(xù)結(jié)點(diǎn)是鏈表結(jié)點(diǎn),則通過(guò)循環(huán)遍歷鏈表根據(jù) key 獲取 value
8. 遍歷HashMap的幾種方式
分別遍歷 Key 和 Values
for (String key : map.keySet()) { System.out.println(key); } for (Object vlaue : map.values() { System.out.println(value); }
使用 Iterator 迭代器迭代
Iterator<Map.Entry<String, Object>> iterator = map.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, Object> mapEntry = iterator.next(); System.out.println(mapEntry.getKey() + "---" + mapEntry.getValue()); }
通過(guò) get 方式(不建議使用)
Set<String> keySet = map.keySet(); for (String str : keySet) { System.out.println(str + "---" + map.get(str)); }
說(shuō)明:
根據(jù)阿里開(kāi)發(fā)手冊(cè),不建議使用這種方式,因?yàn)榈鷥纱?。keySet 獲取 Iterator一次,還有通過(guò) get 又迭代一次,降低性能。
jdk8 以后使用 Map 接口中的默認(rèn)方法:
default void forEach(BiConsumer<? super K,? super V> action) // BiConsumer接口中的方法: void accept(T t, U u) 對(duì)給定的參數(shù)執(zhí)行此操作。 參數(shù) t - 第一個(gè)輸入?yún)?shù) u - 第二個(gè)輸入?yún)?shù)
遍歷代碼:
HashMap<String,String> map = new HashMap(); map.put("001", "zhangsan"); map.put("002", "lisi"); map.forEach((key, value) -> { System.out.println(key + "---" + value); });
9、總結(jié)
(1)HashMap是一種散列表,采用(數(shù)組 + 鏈表 + 紅黑樹(shù))的存儲(chǔ)結(jié)構(gòu);
(2)HashMap的默認(rèn)初始容量為16(1<<4),默認(rèn)裝載因子為0.75f,容量總是2的n次方;
(3)HashMap擴(kuò)容時(shí)每次容量變?yōu)樵瓉?lái)的兩倍;
(4)當(dāng)桶的數(shù)量小于64時(shí)不會(huì)進(jìn)行樹(shù)化,只會(huì)擴(kuò)容;
(5)當(dāng)桶的數(shù)量大于64且單個(gè)桶中元素的數(shù)量大于8時(shí),進(jìn)行樹(shù)化;
(6)當(dāng)單個(gè)桶中元素?cái)?shù)量小于6時(shí),進(jìn)行反樹(shù)化;
(7)HashMap是非線(xiàn)程安全的容器;
(8)HashMap查找添加元素的時(shí)間復(fù)雜度都為O(1);
這篇文章就到這里了,也希望大家多多關(guān)注腳本之家的其他內(nèi)容!
相關(guān)文章
Spring源碼之事件監(jiān)聽(tīng)機(jī)制詳解(@EventListener實(shí)現(xiàn)方式)
這篇文章主要介紹了Spring源碼之事件監(jiān)聽(tīng)機(jī)制(@EventListener實(shí)現(xiàn)方式),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08java volatile關(guān)鍵字作用及使用場(chǎng)景詳解
在本文里我們給大家分享的是關(guān)于java volatile關(guān)鍵字作用及使用場(chǎng)景的相關(guān)知識(shí)點(diǎn)內(nèi)容,需要的朋友們學(xué)習(xí)下。2019-08-08Mybatis的parameterType造成線(xiàn)程阻塞問(wèn)題分析
這篇文章主要詳細(xì)分析了Mybatis的parameterType造成線(xiàn)程阻塞問(wèn)題,文中有詳細(xì)的解決方法,及相關(guān)的代碼示例,具有一定的參考價(jià)值,感興趣的朋友可以借鑒閱讀2023-06-06使用@DS輕松解決動(dòng)態(tài)數(shù)據(jù)源的問(wèn)題
這篇文章主要介紹了使用@DS輕松解決動(dòng)態(tài)數(shù)據(jù)源的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-05-05Springboot訪(fǎng)問(wèn)templates html頁(yè)面過(guò)程詳解
這篇文章主要介紹了Springboot訪(fǎng)問(wèn)templates html頁(yè)面過(guò)程詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05SpringBoot實(shí)現(xiàn)緩存預(yù)熱的幾種常用方案
緩存預(yù)熱是指在 Spring Boot 項(xiàng)目啟動(dòng)時(shí),預(yù)先將數(shù)據(jù)加載到緩存系統(tǒng)(如 Redis)中的一種機(jī)制,本文給大家介紹了SpringBoot實(shí)現(xiàn)緩存預(yù)熱的幾種常用方案,并通過(guò)代碼示例講解的非常詳細(xì),需要的朋友可以參考下2024-02-02如何使用SpringBoot集成Kafka實(shí)現(xiàn)用戶(hù)數(shù)據(jù)變更后發(fā)送消息
Spring Boot集成Kafka實(shí)現(xiàn)用戶(hù)數(shù)據(jù)變更后,向其他廠商發(fā)送消息,我們需要考慮配置Kafka連接、創(chuàng)建Kafka Producer發(fā)送消息、監(jiān)聽(tīng)用戶(hù)數(shù)據(jù)變更事件,并將事件轉(zhuǎn)發(fā)到Kafka,本文分步驟給大家講解使用SpringBoot集成Kafka實(shí)現(xiàn)用戶(hù)數(shù)據(jù)變更后發(fā)送消息,感興趣的朋友一起看看吧2024-07-07java三個(gè)環(huán)境變量配置簡(jiǎn)單教程
這篇文章主要為大家詳細(xì)介紹了java三個(gè)環(huán)境變量配置簡(jiǎn)單教程,配置path變量、配置classpath變量、最后是配置JAVA_HOME變量,感興趣的小伙伴們可以參考一下2016-07-07