欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

入門(mén)JDK集合之HashMap解析

 更新時(shí)間:2021年06月11日 15:10:23   作者:興趣使然的草帽路飛  
HashMap---基于哈希表的 Map 接口的實(shí)現(xiàn)。此實(shí)現(xiàn)提供所有可選的映射操作,并允許使用 null 值和 null 鍵。(除了非同步和允許使用 null 之外,HashMap 類(lèi)與 Hashtable 大致相同

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)一步原因:

  1. 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ì)。
  2. 針對(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)方式?

答:

  1. 對(duì) key 的 hashCode 做 hash 操作,如果key為null則直接賦哈希值為0,否則,無(wú)符號(hào)右移 16 位然后做異或位運(yùn)算,如,代碼所示:(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  2. 除上面的方法外,還有平方取中法,偽隨機(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)容是否相同。

  1. 相同:則新的 value 覆蓋之前的 value。
  2. 不相同:遍歷該桶位的鏈表(或者樹(shù)):
    1. 如果找到相同key,則覆蓋該key對(duì)應(yīng)的value;
    2. 如果找不到,則將新的鍵值對(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)也就越少,也就越稀疏。

數(shù)據(jù)

如果希望鏈表盡可能少些,要提前擴(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ō)明:

  1. HashMap 只提供了 put 用于添加元素,putVal 方法只是給 put 方法調(diào)用的一個(gè)方法,并沒(méi)有提供給用戶(hù)使用。 所以我們重點(diǎn)看 putVal 方法。
  2. 我們可以看到在 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ō)明

  1. key.hashCode();返回散列值也就是 hashcode,假設(shè)隨便生成的一個(gè)值。
  2. n 表示數(shù)組初始化的長(zhǎng)度是 16。
  3. &(按位與運(yùn)算):運(yùn)算規(guī)則:相同的二進(jìn)制數(shù)位上,都是 1 的時(shí)候,結(jié)果為 1,否則為0。
  4. ^(按位異或運(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í),具體的變化如下所示:

擴(kuò)容

因此元素在重新計(jì)算 hash 之后,因?yàn)?n 變?yōu)?2 倍,那么 n - 1 的標(biāo)記范圍在高位多 1bit(紅色),因此新的 index 就會(huì)發(fā)生這樣的變化。

hash

說(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 示意圖:

擴(kuò)容

正是因?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)方式)

    這篇文章主要介紹了Spring源碼之事件監(jiān)聽(tīng)機(jī)制(@EventListener實(shí)現(xiàn)方式),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-08-08
  • java volatile關(guān)鍵字作用及使用場(chǎng)景詳解

    java volatile關(guān)鍵字作用及使用場(chǎng)景詳解

    在本文里我們給大家分享的是關(guān)于java volatile關(guān)鍵字作用及使用場(chǎng)景的相關(guān)知識(shí)點(diǎn)內(nèi)容,需要的朋友們學(xué)習(xí)下。
    2019-08-08
  • JAVA ArrayList詳細(xì)介紹(示例)

    JAVA ArrayList詳細(xì)介紹(示例)

    本文對(duì)JAVA ArrayList做了詳細(xì)介紹,文中學(xué)到了ArrayList源碼解析、ArrayList遍歷方式、toArray()異常,最后給出了ArrayList示例。
    2013-11-11
  • Mybatis的parameterType造成線(xiàn)程阻塞問(wèn)題分析

    Mybatis的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)題

    這篇文章主要介紹了使用@DS輕松解決動(dòng)態(tài)數(shù)據(jù)源的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-05-05
  • Springboot訪(fǎng)問(wèn)templates html頁(yè)面過(guò)程詳解

    Springboot訪(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-05
  • SpringBoot實(shí)現(xiàn)緩存預(yù)熱的幾種常用方案

    SpringBoot實(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
  • idea顯示properties文件中文亂碼的解決方法

    idea顯示properties文件中文亂碼的解決方法

    在項(xiàng)目中通常會(huì)遇到如下問(wèn)題,突然properties文件中文亂碼,本文主要介紹了idea顯示properties文件中文亂碼的解決方法,具有一定的參考價(jià)值,感興趣的可以了解一下
    2023-09-09
  • 如何使用SpringBoot集成Kafka實(shí)現(xiàn)用戶(hù)數(shù)據(jù)變更后發(fā)送消息

    如何使用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-07
  • java三個(gè)環(huán)境變量配置簡(jiǎn)單教程

    java三個(gè)環(huán)境變量配置簡(jiǎn)單教程

    這篇文章主要為大家詳細(xì)介紹了java三個(gè)環(huán)境變量配置簡(jiǎn)單教程,配置path變量、配置classpath變量、最后是配置JAVA_HOME變量,感興趣的小伙伴們可以參考一下
    2016-07-07

最新評(píng)論