亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

入門JDK集合之HashMap解析

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

0.前言

HashMap簡述:

  • HashMap 基于哈希表的 Map 接口實現(xiàn),是以 key-value 存儲形式存在,即主要用來存放鍵值對。HashMap 的實現(xiàn)不是同步的,這意味著它不是線程安全的。它的 key、value 都可以為 null,此外,HashMap 中的映射不是有序的。
  • jdk1.8 之前 HashMap 由 數(shù)組 + 鏈表 組成,數(shù)組是 HashMap 的主體,鏈表則是主要為了解決哈希沖突(兩個對象調(diào)用的 hashCode 方法計算的哈希值經(jīng)哈希函數(shù)算出來的地址被別的元素占用)而存在的(“拉鏈法”解決沖突)。jdk1.8 以后在解決哈希沖突時有了較大的變化,當鏈表長度大于閾值(或者紅黑樹的邊界值,默認為 8 )并且當前數(shù)組的長度大于 64 時,此時此索引位置上的所有數(shù)據(jù)改為使用紅黑樹存儲。

HashMap擴容機制簡述:

HashMap == 數(shù)組+散鏈表+紅黑樹

  • HashMap 默認初始桶位數(shù)16,如果某個桶中的鏈表長度大于8,則先進行判斷:
  • 如果桶位數(shù)小于64,則先進行擴容(2倍),擴容之后重新計算哈希值,這樣桶中的鏈表長度就變短了(之所以鏈表長度變短與桶的定位方式有關(guān),請接著往下看)。
  • 如果桶位數(shù)大于64,且某個桶中的鏈表長度大于8,則對鏈表進行樹化(紅黑樹,即自平衡的二叉樹)
  • 如果紅黑樹的節(jié)點數(shù)小于6,樹也會重新變會鏈表。

所以得出樹化條件:鏈表閾值大于8,且桶位數(shù)大于64(數(shù)組長度),才進行樹化。

元素放入桶(數(shù)組)中,定位桶的方式:通過數(shù)組下標 i 定位,添加元素時,目標桶位置 i 的計算公式,i = hash & (cap - 1),cap為容量。

為什么優(yōu)先擴容桶位數(shù)(數(shù)組長度),而不是直接樹化?

  • 這樣做的目的是因為,當桶位數(shù)(數(shù)組長度)比較小時,應(yīng)盡量避開紅黑樹結(jié)構(gòu),這種情況下變?yōu)榧t黑樹結(jié)構(gòu),反而會降低效率。因為紅黑樹需要逬行左旋,右旋,變色這些操作來保持平衡。同時數(shù)組長度小于64時,搜索時間相對要快些。所以結(jié)上所述為了提高性能和減少搜索時間,底層閾值大于8并且數(shù)組長度大于64時,鏈表才轉(zhuǎn)換為紅黑樹,具體可以參考下文要講述的 treeifyBin() 方法。
  • 而當閾值大于 8 并且數(shù)組長度大于 64 時,雖然增了紅黑樹作為底層數(shù)據(jù)結(jié)構(gòu),結(jié)構(gòu)變得復雜了,但是,長度較長的鏈表轉(zhuǎn)換為紅黑樹時,效率也變高了。

HashMap 特點:

  • 存儲無序;
  • 鍵和值位置都可以是 null,但是鍵位置只能存在一個 null;
  • 鍵位置是唯一的,是由底層的數(shù)據(jù)結(jié)構(gòu)控制的;jdk1.8 前數(shù)據(jù)結(jié)構(gòu)是鏈表+數(shù)組,jdk1.8 之后是鏈表+數(shù)組+紅黑樹;
  • 閾值(邊界值)> 8 并且桶位數(shù)(數(shù)組長度)大于 64,才將鏈表轉(zhuǎn)換為紅黑樹,變?yōu)榧t黑樹的目的是為了高效的查詢;

1.HashMap存儲數(shù)據(jù)的過程

注意:相對于直接去讀HashMap源碼來說,先debug一下其執(zhí)行數(shù)據(jù)存儲的流程,更方便大家理解!

測試代碼:

@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();當創(chuàng)建 HashMap 集合對象的時候,HashMap 的構(gòu)造方法并沒有創(chuàng)建數(shù)組,而是在第一次調(diào)用 put 方法時創(chuàng)建一個長度是16 的數(shù)組(即,16個桶) ,Node[] table (jdk1.8 之前是 Entry[] table)用來存儲鍵值對數(shù)據(jù)。

2.當向哈希表中存儲put("a", 3)的數(shù)據(jù)時,根據(jù)"a"字符串調(diào)用 String 類中重寫之后的 hashCode() 方法計算出哈希值,然后結(jié)合數(shù)組長度(桶數(shù)量)采用某種算法計算出向 Node 數(shù)組中存儲數(shù)據(jù)的空間索引值(比如table[i],這里的i就是該Node數(shù)組的空間索引)。如果計算出的索引空間沒有數(shù)據(jù)(即,這個桶是空的),則直接將<"a", 3>存儲到數(shù)組中。

舉例:如果計算出的索引是 3,則存儲到如下桶位:

在這里插入圖片描述

3.當向哈希表中存儲數(shù)據(jù)<"b", 4>時,假設(shè)算出的 hashCode() 方法結(jié)合數(shù)祖長度計算出的索引值也是3,那么此時數(shù)組空間不是 null(即,這個桶目前不為空),此時底層會比較 "a""b" 的 hash 值是否一致,如果不一致,則在空間上劃出一個結(jié)點來存儲鍵值對數(shù)據(jù)對 <"b", 4>,這種方式稱為拉鏈法。

4.當向哈希表中存儲數(shù)據(jù)<"a", 88888>時,那么首先根據(jù) "a"調(diào)用 hashCode() 方法結(jié)合數(shù)組長度計算出索引肯定是 3,此時比較后存儲的數(shù)據(jù)"a"和已經(jīng)存在的數(shù)據(jù)的 hash 值是否相等,如果 hash 值相等,此時發(fā)生哈希碰撞。那么底層會調(diào)用 "a"所屬類 String 中的 equals() 方法比較兩個內(nèi)容是否相等:

相等:將后添加的數(shù)據(jù)的 value 覆蓋之前的 value。

不相等:繼續(xù)向下和其他的數(shù)據(jù)的 key 進行比較,如果都不相等,則劃出一個結(jié)點存儲數(shù)據(jù),如果結(jié)點長度即鏈表長度大于閾值 8 并且數(shù)組長度大于 64 則將鏈表變?yōu)榧t黑樹。


在這里插入圖片描述

5.在不斷的添加數(shù)據(jù)的過程中,會涉及到擴容問題,當超出閾值(且要存放的位置非空)時,擴容。默認的擴容方式:擴容為原來容量的 2 倍,并將原有的數(shù)據(jù)復制過來。

6.綜上描述,當位于一個表中的元素較多,即 hash 值相等但是內(nèi)容不相等的元素較多時,通過 key 值依次查找的效率較低。而 jdk1.8 中,哈希表存儲采用數(shù)組+鏈表+紅黑樹實現(xiàn),當鏈表長度(閾值)超過8且當前數(shù)組的長度大于64時,將鏈表轉(zhuǎn)換為紅黑樹,這樣大大減少了查找時間。

簡單的來說,哈希表是由數(shù)組+鏈表+紅黑樹(JDK1.8增加了紅黑樹部分)實現(xiàn)的。如下圖所示:


在這里插入圖片描述

7.jdk1.8 中引入紅黑樹的進一步原因:

  1. jdk1.8 以前 HashMap 的實現(xiàn)是數(shù)組+鏈表,即使哈希函數(shù)取得再好,也很難達到元素百分百均勻分布。當 HashMap 中有大量的元素都存放到同一個桶中時,這個桶下有一條長長的鏈表,這個時候 HashMap 就相當于一個單鏈表,假如單鏈表有n個元素,遍歷的時間復雜度就是O(n),完全失去了它的優(yōu)勢。
  2. 針對這種情況,jdk1.8 中引入了紅黑樹(查找時間復雜度為 O(logn))來優(yōu)化這個問題。當鏈表長度很小的時候,即使遍歷,速度也非??欤钱旀湵黹L度不斷變長,肯定會對查詢性能有一定的影響,所以才需要轉(zhuǎn)成樹。

8.總結(jié)

在這里插入圖片描述

說明:

  • size 表示 HashMap 中鍵值對的實時數(shù)量(即,所存儲元素的數(shù)量),注意這個不等于數(shù)組的長度。
  • threshold(臨界值)= capacity(容量)* loadFactor(負載因子)。這個值是當前已占用數(shù)組長度的最大值。size 超過這個值就重新 resize(擴容),擴容后的 HashMap 容量是之前容量的2倍。

2.HashMap相關(guān)面試題

具體原理我們下文會具體分析,這里先大概了解下面試的時候會問什么,帶著問題去讀源碼,便于理解!

1.shMap 中 hash 函數(shù)是怎么實現(xiàn)的?還有哪些hash函數(shù)的實現(xiàn)方式?

答:

  1. 對 key 的 hashCode 做 hash 操作,如果key為null則直接賦哈希值為0,否則,無符號右移 16 位然后做異或位運算,如,代碼所示:(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  2. 除上面的方法外,還有平方取中法,偽隨機數(shù)法 和 取余數(shù)法。這三種效率都比較低,而無符號右移 16 位異或運算效率是最高的。

2.當兩個對象的 hashCode 相等時會怎么樣?

答:會產(chǎn)生哈希碰撞。若 key 值內(nèi)容相同則替換舊的 value,不然連接到鏈表后面,鏈表長度超過閾值 8 就轉(zhuǎn)換為紅黑樹存儲。

3.什么是哈希碰撞,如何解決哈希碰撞?

答:只要兩個元素的 key 計算的哈希碼值相同就會發(fā)生哈希碰撞。jdk8 之前使用鏈表解決哈希碰撞。jdk8之后使用鏈表 + 紅黑樹解決哈希碰撞。

4.如果兩個鍵的 hashCode 相同,如何存儲鍵值對?

答:通過 equals 比較內(nèi)容是否相同。

  1. 相同:則新的 value 覆蓋之前的 value。
  2. 不相同:遍歷該桶位的鏈表(或者樹):
    1. 如果找到相同key,則覆蓋該key對應(yīng)的value;
    2. 如果找不到,則將新的鍵值對添加到鏈表(或者樹)中;

3.HashMap繼承體系

在這里插入圖片描述
從繼承體系可以看出:

  • HashMap 實現(xiàn)了Cloneable接口,可以被克隆。
  • HashMap 實現(xiàn)了Serializable接口,屬于標記性接口,HashMap 對象可以被序列化和反序列化。
  • HashMap 繼承了AbstractMap,父類提供了 Map 實現(xiàn)接口,具有Map的所有功能,以最大限度地減少實現(xiàn)此接口所需的工作。

知識擴展:

通過上述繼承關(guān)系我們發(fā)現(xiàn)一個很奇怪的現(xiàn)象,就是 HashMap 已經(jīng)繼承了AbstractMap 而 AbstractMap 類實現(xiàn)了Map 接口,那為什么 HashMap 還要在實現(xiàn) Map 接口呢?同樣在 ArrayList 中 LinkedLis 中都是這種結(jié)構(gòu)。

據(jù) Java 集合框架的創(chuàng)始人 Josh Bloch 描述,這樣的寫法是一個失誤。在 Java 集合框架中,類似這樣的寫法很多,最幵始寫 Java 集合框架的時候,他認為這樣寫,在某些地方可能是有價值的,直到他意識到錯了。顯然的,jdk 的維護者,后來不認為這個小小的失誤值得去修改,所以就這樣保留下來了。

存儲結(jié)構(gòu)(再過一遍)


在這里插入圖片描述

在Java中,HashMap的實現(xiàn)采用了(數(shù)組 + 鏈表 + 紅黑樹)的復雜結(jié)構(gòu),數(shù)組的一個元素又稱作桶。

在添加元素時,會根據(jù)hash值算出元素在數(shù)組中的位置,如果該位置沒有元素,則直接把元素放置在此處,如果該位置有元素了,則把元素以鏈表的形式放置在鏈表的尾部。

當一個鏈表的元素個數(shù)達到一定的數(shù)量(且數(shù)組的長度達到一定的長度)后,則把鏈表轉(zhuǎn)化為紅黑樹,從而提高效率

數(shù)組的查詢效率為O(1),鏈表的查詢效率是O(k),紅黑樹的查詢效率是O(log k),k為桶中的元素個數(shù),所以當元素數(shù)量非常多的時候,轉(zhuǎn)化為紅黑樹能極大地提高效率。

4.HashMap基本屬性與常量

/*
 * 序列化版本號
 */
private static final long serialVersionUID = 362498820763181265L;
/**
 * HashMap的初始化容量(必須是 2 的 n 次冪)默認的初始容量為16
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/**
 * 最大的容量為2的30次方
 */
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
 * 默認的裝載因子
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
 * 樹化閾值,當一個桶中的元素個數(shù)大于等于8時進行樹化
 */
static final int TREEIFY_THRESHOLD = 8;
/**
 * 樹降級為鏈表的閾值,當一個桶中的元素個數(shù)小于等于6時把樹轉(zhuǎn)化為鏈表
 */
static final int UNTREEIFY_THRESHOLD = 6;
/**
 * 當桶的個數(shù)達到64的時候才進行樹化
 */
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ù),用于在迭代的時候執(zhí)行快速失敗策略
 */
transient int modCount;
/**
 * 當桶的使用數(shù)量達到多少時進行擴容,threshold = capacity * loadFactor
 */
int threshold;
/**
 * 裝載因子
 */
final float loadFactor;

(1)容量:容量為數(shù)組的長度,亦即桶的個數(shù),默認為16 ,最大為2的30次方,當容量達到64時才可以樹化。

(2)裝載因子:裝載因子用來計算容量達到多少時才進行擴容,默認裝載因子為0.75。

(3)樹化:樹化,當容量達到64且鏈表的長度達到8時進行樹化,當鏈表的長度小于6時反樹化。

4.1 DEFAULT_INITIAL_CAPACITY

集合的初始化容量(必須是 2 的 n 次冪):

// 默認的初始容量是16	1 << 4 相當于 1*2的4次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

面試問題:為什么必須是 2 的 n 次冪?如果輸入值不是 2 的冪比如 10 會怎么樣?

HashMap 構(gòu)造方法可以指定集合的初始化容量大小,如:

// 構(gòu)造一個帶指定初始容量和默認負載因子(0.75)的空 HashMap。
HashMap(int initialCapacity)

根據(jù)上述講解我們已經(jīng)知道,當向 HashMap 中添加一個元素的時候,需要根據(jù) key 的 hash 值,去確定其在數(shù)組中的具體位置。HashMap 為了存取高效,減少碰撞,就是要盡量把數(shù)據(jù)分配均勻,每個鏈表長度大致相同,這個實現(xiàn)的關(guān)鍵就在把數(shù)據(jù)存到哪個鏈表中的算法。

這個算法實際就是取模,hash % length,而計算機中直接求余效率不如位移運算。所以源碼中做了優(yōu)化,使用 hash & (length - 1),而實際上 hash % length 等于 hash & ( length - 1) 的前提是 length 是 2 的 n 次冪。(這段話是摘抄傳智播客鎖哥的,這個解釋確實很完美!)

例如,數(shù)組長度為 8 的時候,3 & (8 - 1) = 3,2 & (8 - 1) = 2,桶的位置是(數(shù)組索引)3和2,不同位置上,不碰撞。

再來看一個數(shù)組長度(桶位數(shù))不是2的n次冪的情況:

在這里插入圖片描述

從上圖可以看出,當數(shù)組長度為9(非2 的n次冪)的時候,不同的哈希值hash, hash & (length - 1)所得到的數(shù)組下標相等(很容易出現(xiàn)哈希碰撞)。

小結(jié)一下HashMap數(shù)組容量使用2的n次冪的原因:(面試也會問)

在這里插入圖片描述


問題:如果創(chuàng)建HashMap對象時,輸入的數(shù)組長度length是10,而不是2的n次冪會怎么樣呢?

HashMap<String, Integer> hashMap = new HashMap(10);

HashMap雙參構(gòu)造函數(shù)會通過tableSizeFor(initialCapacity)方法,得到一個最接近length且大于length的2的n次冪數(shù)(比如最接近10且大于10的2的n次冪數(shù)是16)

這一塊兒比較難理解,下文講構(gòu)造方法的時候還會再舉例一個例子:

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;
}

說明:

當在實例化 HashMap 實例時,如果給定了 initialCapacity,由于 HashMap 的 capacity 必須是 2 的冪,因此這個方法tableSizeFor(initialCapacity);用于找到大于等于 initialCapacity 的最小的 2 的冪。

分析:

int n = cap - 1;為什么要減去1呢?

防止 cap 已經(jīng)是 2 的冪。如果 cap 已經(jīng)是 2 的冪,又沒有這個減 1 操作,則執(zhí)行完后面的幾條無符號操作之后,返回的 capacity 將是這個 cap 的 2 倍(后面還會再舉個例子講這個)。

最后為什么有個 n + 1 的操作呢?

如果 n 這時為 0 了(經(jīng)過了cap - 1后),則經(jīng)過后面的幾次無符號右移依然是 0,返回0是肯定不行的,所以最后返回n+1最終得到的 capacity 是1。

注意:容量最大也就是 32bit 的正數(shù),因此最后 n |= n >>> 16;最多也就 32 個 1(但是這已經(jīng)是負數(shù)了,在執(zhí)行 tableSizeFor 之前,對 initialCapacity 做了判斷,如果大于MAXIMUM_CAPACITY(2 ^ 30),則取 MAXIMUM_CAPACITY。如果等于MAXIMUM_CAPACITY,會執(zhí)行位移操作。所以這里面的位移操作之后,最大 30 個 1,不會大于等于 MAXIMUM_CAPACITY。30 個 1,加 1 后得 2 ^ 30)。

完整例子:

在這里插入圖片描述

所以由結(jié)果可得,當執(zhí)行完tableSizeFor(initialCapacity);方法后,得到的新capacity是最接近initialCapacity且大于initialCapacity的2的n次冪的數(shù)。

4.2 DEFAULT_LOAD_FACTOR

默認的負載因子(默認值 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

當鏈表的值超過8則會轉(zhuǎn)為紅黑樹(jdk1.8新增)

// 當桶(bucket)上的結(jié)點數(shù)大于這個值時會轉(zhuǎn)為紅黑樹
static final int TREEIFY_THRESHOLD = 8;

面試問題:為什么 Map 桶中結(jié)點個數(shù)超過 8 才轉(zhuǎn)為紅黑樹?

8這個閾值定義在HashMap中,針對這個成員變量,在源碼的注釋中只說明了 8 是 bin( bucket 桶)從鏈表轉(zhuǎn)成樹的閾值,但是并沒有說明為什么是 8。

在 HashMap 中有一段注釋說明:

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:
翻譯:因為樹結(jié)點的大小大約是普通結(jié)點的兩倍,所以我們只在箱子包含足夠的結(jié)點時才使用樹結(jié)點(參見TREEIFY_THRESHOLD)。
當它們變得太?。ㄓ捎趧h除或調(diào)整大小)時,就會被轉(zhuǎn)換回普通的桶。在使用分布良好的用戶 hashCode 時,很少使用樹箱。
理想情況下,在隨機哈希碼下,箱子中結(jié)點的頻率服從泊松分布。
默認調(diào)整閾值為0.75,平均參數(shù)約為0.5,盡管由于調(diào)整粒度的差異很大。忽略方差,列表大小k的預朗出現(xiàn)次數(shù)是(exp(-0.5)*pow(0.5, k) / factorial(k)
第一個值是:
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(樹) 占用空間是普通 Nodes(鏈表) 的兩倍,所以只有當 bin(bucket 桶) 包含足夠多的結(jié)點時才會轉(zhuǎn)成 TreeNodes,而是否足夠多就是由 TREEIFY_THRESH〇LD 的值決定的。當 bin(bucket 桶) 中結(jié)點數(shù)變少時,又會轉(zhuǎn)成普通的 bin(bucket 桶)。并且我們查看源碼的時候發(fā)現(xiàn),鏈表長度達到 8 就轉(zhuǎn)成紅黑樹,當長度降到 6 就轉(zhuǎn)成普通 bin(bucket 桶)。

這樣就解釋了為什么不是一開始就將其轉(zhuǎn)換為 TreeNodes,而是需要一定結(jié)點數(shù)之后才轉(zhuǎn)為 TreeNodes,說白了就是權(quán)衡空間和時間。

這段內(nèi)容還說到:當 hashCode 離散性很好的時候,樹型 bin 用到的概率非常小,因為數(shù)據(jù)均勻分布在每個 bin 中,幾乎不會有 bin 中鏈表長度會達到閾值。但是在隨機 hashCode 下,離散性可能會變差,然而 jdk 又不能阻止用戶實現(xiàn)這種不好的 hash 算法,因此就可能導致不均勻的數(shù)據(jù)分布。不理想情況下隨機 hashCode 算法下所有 bin 中結(jié)點的分布頻率會遵循泊松分布,我們可以看到,一個 bin 中鏈表長度達到 8 個元素的槪率為 0.00000006,幾乎是不可能事件。所以,之所以選擇 8,不是隨便決定的,而是裉據(jù)概率統(tǒng)計決定的。甶此可見,發(fā)展將近30年的 Java 每一項改動和優(yōu)化都是非常嚴謹和科學的。

面試答案:hashCode 算法下所有 桶 中結(jié)點的分布頻率會遵循泊松分布,這時一個桶中鏈表長度超過 8 個元素的槪率非常小,權(quán)衡空間和時間復雜度,所以選擇 8 這個數(shù)宇。

擴展補充:

Poisson 分布(泊松分布),是一種統(tǒng)計與概率學里常見到的離散[概率分布]。泊松分布的概率函數(shù)為:

公式

泊松分布的參數(shù) A 是單位時間(或單位面積)內(nèi)隨機事件的平均發(fā)生次數(shù)。泊松分布適合于描述單位時間內(nèi)隨機事件發(fā)生的次數(shù)。

以下是我在研究這個問題時,在一些資料上面翻看的解釋,供大家參考:

紅黑樹的平均查找長度是 log(n),如果長度為 8,平均查找長度為 log(8) = 3,鏈表的平均查找長度為 n/2,當長度為 8 時,平均查找長虔為 8/2 = 4,這才有轉(zhuǎn)換成樹的必要;鏈表長度如果是小于等于 6, 6/2 = 3,而 log(6) = 2.6,雖然速度也很快的,但是轉(zhuǎn)化為樹結(jié)構(gòu)和生成樹的時間并不會太短。

4.5 UNTREEIFY_THRESHOLD

當鏈表的值小于 6 則會從紅黑樹轉(zhuǎn)回鏈表

// 當桶(bucket)上的結(jié)點數(shù)小于這個值,樹轉(zhuǎn)為鏈表 
static final int UNTREEIFY_THRESHOLD = 6;

4.6 MIN_TREEIFY_CAPACITY

當 Map 里面的數(shù)量超過這個值時,表中的桶才能進行樹形化,否則桶內(nèi)元素太多時會擴容,而不是樹形化為了避免進行擴容、樹形化選擇的沖突,這個值不能小于4*TREEIFY_THRESHOLD(8)

// 桶中結(jié)構(gòu)轉(zhuǎn)化為紅黑樹對應(yīng)的數(shù)組長度最小的值 
static final int MIN_TREEIFY_CAPACITY = 64;

4.7 table(重點)

table 用來初始化(必須是二的n次冪)

// 存儲元素的數(shù)組 
transient Node<K,V>[] table;

在 jdk1.8 中我們了解到 HashMap 是由數(shù)組加鏈表加紅黑樹來組成的結(jié)構(gòu),其中 table 就是 HashMap 中的數(shù)組,jdk8 之前數(shù)組類型是 Entry<K,V> 類型。從 jdk1.8 之后是 Node<K,V> 類型。只是換了個名字,都實現(xiàn)了一樣的接口:Map.Entry<K,V>。負責存儲鍵值對數(shù)據(jù)的。

4.8 entrySet

用來存放緩存

// 存放具體元素的集合
transient Set<Map.Entry<K,V>> entrySet;

4.9 size(重點)

HashMap 中存放元素的個數(shù)

// 存放元素的個數(shù),注意這個不等于數(shù)組的長度
 transient int size;

size 為 HashMap 中 K-V 的實時數(shù)量,不是數(shù)組 table 的長度。

4.10 modCount

用來記錄 HashMap 的修改次數(shù)

// 每次擴容和更改 map 結(jié)構(gòu)的計數(shù)器
 transient int modCount;  

4.11 threshold(重點)

用來調(diào)整大小下一個容量的值計算方式為(容量*負載因子)

// 臨界值 當實際大?。ㄈ萘?負載因子)超過臨界值時,會進行擴容
int threshold;

4.12 loadFactor(重點)

哈希表的負載因子

// 負載因子
final float loadFactor;// 0.75f

說明:

loadFactor 是用來衡量 HashMap 滿的程度,表示HashMap的疏密程度,影響 hash 操作到同一個數(shù)組位置的概率,計算 HashMap 的實時負載因子的方法為:size/capacity,而不是占用桶的數(shù)量去除以 capacity。capacity 是桶的數(shù)量,也就是 table 的長度 length。loadFactor 太大導致查找元素效率低,太小導致數(shù)組的利用率低,存放的數(shù)據(jù)會很分散。loadFactor 的默認值為 0.75f 是官方給出的一個比較好的臨界值。當 HashMap 里面容納的元素已經(jīng)達到 HashMap 數(shù)組長度的 75% 時,表示 HashMap 太擠了,需要擴容,而擴容這個過程涉及到 rehash、復制數(shù)據(jù)等操作,非常消耗性能。所以開發(fā)中盡量減少擴容的次數(shù),可以通過創(chuàng)建 HashMap 集合對象時指定初始容量來盡量避免。在 HashMap 的構(gòu)造器中可以定制 loadFactor。

// 構(gòu)造方法,構(gòu)造一個帶指定初始容量和負載因子的空HashMap
HashMap(int initialCapacity, float loadFactor);

為什么負載因子loadFactor 設(shè)置為0.75,初始化臨界值threshold是12?

loadFactor 越趨近于1,那么 數(shù)組中存放的數(shù)據(jù)(entry)也就越多,也就越密,也就是會讓鏈表的長度增加,loadFactor 越小,也就是趨近于0,數(shù)組中存放的數(shù)據(jù)(entry)也就越少,也就越稀疏。

數(shù)據(jù)

如果希望鏈表盡可能少些,要提前擴容。有的數(shù)組空間有可能一直沒有存儲數(shù)據(jù),負載因子盡可能小一些。

舉例:

例如:負載因子是0.4。 那么16*0.4--->6 如果數(shù)組中滿6個空間就擴容會造成數(shù)組利用率太低了。
負載因子是0.9。 那么16*0.9--->14 那么這樣就會導致鏈表有點多了,導致查找元素效率低。

所以既兼顧數(shù)組利用率又考慮鏈表不要太多,經(jīng)過大量測試 0.75 是最佳方案。

threshold 計算公式:capacity(數(shù)組長度默認16) * loadFactor(負載因子默認0.75)==12。

這個值是當前已占用數(shù)組長度的最大值。當 Size >= threshold(12) 的時候,那么就要考慮對數(shù)組的 resize(擴容),也就是說,這個的意思就是 衡量數(shù)組是否需要擴增的一個標準。 擴容后的 HashMap 容量是之前容量的兩倍。

5.內(nèi)部類

5.1Node內(nèi)部類

Node是一個典型的單鏈表節(jié)點,其中,hash用來存儲key計算得來的hash值。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;// hash用來存儲key計算得來的hash值
    final K key;// 鍵
    V value;// 值
    Node<K,V> next;// 下一個node節(jié)點
    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的哈希碼值,如果此對象為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)部類

TreeNode內(nèi)部類,它繼承自LinkedHashMap中的Entry類,關(guān)于LInkedHashMap.Entry這個類之后會單獨發(fā)文章論述,TreeNode是一個典型的樹型節(jié)點,其中,prev是鏈表中的節(jié)點,用于在刪除元素的時候可以快速找到它的前置節(jié)點。

// 位于HashMap中,文章接下來會逐步分析
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é)點,這個類之后會單獨發(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)造一個空的HashMap,默認初始容量(16)和默認負載因子(0.75)。

public HashMap() {
   // 將默認的負載因子0.75賦值給loadFactor,并沒有創(chuàng)建數(shù)組
   this.loadFactor = DEFAULT_LOAD_FACTOR; 
}

6.2 HashMap(int initialCapacity)

構(gòu)造一個具有指定的初始容量和默認負載因子(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)造一個具有指定的初始容量和負載因子的 HashMap。

/*
	 指定“容量大小”和“負載因子”的構(gòu)造函數(shù)
	 initialCapacity:指定的容量
	 loadFactor:指定的負載因子
*/
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)
            // 如果超過MAXIMUM_CAPACITY,會將MAXIMUM_CAPACITY賦值給initialCapacity
            initialCapacity = MAXIMUM_CAPACITY;
    	// 判斷負載因子loadFactor是否小于等于0或者是否是一個非數(shù)值
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            // 如果滿足上述其中之一,則拋出非法的參數(shù)異常IllegalArgumentException
            throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
     	// 將指定的負載因子賦值給HashMap成員變量的負載因子loadFactor
        this.loadFactor = loadFactor;// 一般不建議修改默認的負載因子
        this.threshold = tableSizeFor(initialCapacity);
    }
	// 最后調(diào)用了tableSizeFor,來看一下方法實現(xiàn):
     /*
     	返回比指定cap容量大的最小2的n次冪數(shù):
     	前面第一遍講述的應(yīng)該有些小伙伴難以理解,這里我在舉例解析一下:
     	-------------------------------------------------------
     	首先假定傳入的cap = 10
     	則,n = 10 -1 => 9
     	n |= n >>> 1 就等同于 n = (n | n >>> 1),所以:
     	(位運算不懂的可以去看我的《Java基礎(chǔ)提高之位運算》這篇文章)
     	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的話,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
        而實際情況是應(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)過上面運算后得到:還是15
        返回結(jié)果:
        return 15 + 1 = 16
        所以我們得出結(jié)果:
        防止 cap 已經(jīng)是 2 的冪數(shù)情況下。沒有這個減 1 操作,
        則執(zhí)行完幾條無符號位移或位運算操作之后,返回的cap(32)將是實際所需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;
    }

說明:

對于this.threshold = tableSizeFor(initialCapacity); 疑問?

**tableSizeFor(initialCapacity)**判斷指定的初始化容量是否是2的n次冪,如果不是那么會變?yōu)楸戎付ǔ跏蓟萘看蟮淖钚〉?的n次冪。

但是注意,在tableSizeFor方法體內(nèi)部將計算后的數(shù)據(jù)返回給調(diào)用這里了,并且直接賦值給threshold邊界值了。有些人會覺得這里是一個bug,應(yīng)該這樣書寫:

this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;

這樣才符合threshold的意思(當HashMap的size到達threshold這個閾值時會擴容)

但是請注意,在jdk8以后的構(gòu)造方法中,并沒有對table這個成員變量進行初始化,table的初始化被推遲到了put方法中,在put方法中會對threshold重新計算。

6.4 HashMap(Map<? extends K, ? extends V> m)

包含另一個 “Map” 的構(gòu)造函數(shù)

// 構(gòu)造一個映射關(guān)系與指定 Map 相同的新 HashMap。
public HashMap(Map<? extends K, ? extends V> m) {
    	// 負載因子loadFactor變?yōu)槟J的負載因子0.75
         this.loadFactor = DEFAULT_LOAD_FACTOR;
         putMapEntries(m, false);
 }

最后調(diào)用了 putMapEntries(),來看一下方法實現(xiàn):

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    //獲取參數(shù)集合的長度
    int s = m.size();
    if (s > 0) {//判斷參數(shù)集合的長度是否大于0
        if (table == null) { // 判斷table是否已經(jīng)初始化
                // 未初始化,s為m的實際元素個數(shù)
                float ft = ((float)s / loadFactor) + 1.0F;// 得到新的擴容閾值
                int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);// 新的擴容閾值float自動向下轉(zhuǎn)型為int
                // 計算得到的t大于閾值,則初始化閾值,將其變?yōu)榉弦蟮?的n次冪數(shù)
                if (t > threshold)
                    threshold = tableSizeFor(t);
        }
        // 如果table已初始化過了,并且m元素個數(shù)大于閾值,進行擴容處理
        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é)):

面試問題:float ft = ((float)s / loadFactor) + 1.0F; 這一行代碼中為什么要加 1.0F ?

(float)s/loadFactor 的結(jié)果是小數(shù),加 1.0F 與 (int)ft 相當于是對小數(shù)做一個向上取整以盡可能的保證更大容量,更大的容量能夠減少 resize 的調(diào)用次數(shù)(為了效率,應(yīng)當盡量減少擴容的次數(shù))。所以 + 1.0F 是為了獲取更大的容量。

例如:原來集合的元素個數(shù)是 6 個,那么 6/0.75 是8,由于8是 2 的n次冪,那么

if (t > threshold) threshold = tableSizeFor(t);執(zhí)行過后,新的數(shù)組大小就是 8 了。然后原來數(shù)組的數(shù)據(jù)就會存儲到長度是 8 的新的數(shù)組中了,這樣會導致在存儲元素的時候,容量不夠,還得繼續(xù)擴容,那么性能降低了,而如果 +1 呢,數(shù)組長度直接變?yōu)?6了,這樣可以減少數(shù)組的擴容。

7.HashMap的成員方法

7.1 put(K key, V value)方法

put方法是比較復雜的,實現(xiàn)步驟大致如下:

1.先通過 hash 值計算出 key 映射到哪個桶;

2.如果桶上沒有碰撞沖突,則直接插入;

3.如果出現(xiàn)碰撞沖突了,則需要處理沖突:

a 如果該桶使用紅黑樹處理沖突,則調(diào)用紅黑樹的方法插入數(shù)據(jù);

b 否則采用傳統(tǒng)的鏈式方法插入。如果鏈的長度達到臨界值,則把鏈轉(zhuǎn)變?yōu)榧t黑樹;

4.如果桶中存在重復的鍵,則為該鍵替換新值 value;

5.如果 size 大于閾值 threshold,則進行擴容;

具體的方法如下:

public V put(K key, V value) {
    // 調(diào)用hash(key)計算出key的hash值
    return putVal(hash(key), key, value, false, true);
}

說明:

  1. HashMap 只提供了 put 用于添加元素,putVal 方法只是給 put 方法調(diào)用的一個方法,并沒有提供給用戶使用。 所以我們重點看 putVal 方法。
  2. 我們可以看到在 putVal 方法中 key 在這里執(zhí)行了一下 hash 方法,來看一下 hash 方法是如何實現(xiàn)的。
static final int hash(Object key) {
int h;
// 如果key為null,則hash值為0,
// 否則調(diào)用key的hashCode()方法計算出key的哈希值然后賦值給h,
// 后與h無符號右移16位后的二進制進行按位異或得到最后的hash值,
// 這樣做是為了使計算出的hash更分散
// 為什么要更分散呢?因為越分散,某個桶的鏈表長度就越短,之后生成的紅黑樹越少,效率越高
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

從上面可以得知 HashMap 是支持 key 為空的,而 HashTable 是直接用 Key 來獲取hashCode 所以 key 為空會拋異常。

解讀上述 hash 方法:

我們先研究下 key 的哈希值是如何計算出來的。key 的哈希值是通過上述方法計算出來的。

這個哈希方法首先計算出 key 的 hashCode 賦值給 h,然后與 h 無符號右移 16 位后的二進制進行按位異或得到最后的 hash 值。

在 putVal 函數(shù)中使用到了上述 hash 函數(shù)計算的哈希值:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
	...
	if ((p = tab[i = (n - 1) & hash]) == null) // 這里的n表示數(shù)組長度16 ,公式
    // (length - 1) & hash = 桶位下標 當數(shù)組長度為2的n次冪數(shù)時,
    // 該公式相當于:hash % length 哈希值對數(shù)組長度取余
    // 例如:hash % 32 = hash & (32-1)
	...
}

計算過程如下所示:

說明

  1. key.hashCode();返回散列值也就是 hashcode,假設(shè)隨便生成的一個值。
  2. n 表示數(shù)組初始化的長度是 16。
  3. &(按位與運算):運算規(guī)則:相同的二進制數(shù)位上,都是 1 的時候,結(jié)果為 1,否則為0。
  4. ^(按位異或運算):運算規(guī)則:相同的二進制數(shù)位上,數(shù)字相同,結(jié)果為 0,不同為 1。

最后獲得0101==> 下標為5的捅。

簡單來說就是:

高 16bit 不變,低 16bit 和高 16bit 做了一個異或(得到的 hashCode 轉(zhuǎn)化為 32 位二進制,前 16 位和后 16 位低 16bit 和高 16bit 做了一個異或)。

問題:為什么要這樣操作呢?

如果當 n 即數(shù)組長度很小,假設(shè)是 16 的話,那么 n - 1 即為 1111 ,這樣的值和 hashCode 直接做按位與操作,實際上只使用了哈希值的后 4 位。如果當哈希值的高位變化很大,低位變化很小,這樣就很容易造成哈希沖突了,所以這里把高低位都利用起來,從而解決了這個問題。

在這里插入圖片描述

下面,我們來看看 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位參與整個hash異或,如果數(shù)組長度比較小的情況下,讓高16位也參與尋址,
    // 尋址公式:(length - 1) & hash 
    // 這樣可以使計算出的結(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; 表示存儲Map集合中元素的數(shù)組。
    	2)(tab = table) == null 表示將空的table賦值給tab,然后判斷tab是否等于null,第一次肯定是null。
    	3)(n = tab.length) == 0 表示將數(shù)組的長度0賦值給n,然后判斷n是否等于0,n等于0,由于if判斷使用雙或,滿足一個即可,則執(zhí)行代碼 n = (tab = resize()).length; 進行數(shù)組初始化,并將初始化好的數(shù)組長度賦值給n。
    	4)執(zhí)行完n = (tab = resize()).length,數(shù)組tab每個空間都是null。
    */
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    /*
    	1)i = (n - 1) & hash 表示計算數(shù)組的索引賦值給i,即確定元素存放在哪個桶中。
    	2)p = tab[i = (n - 1) & hash]表示獲取計算出的位置的數(shù)據(jù)賦值給結(jié)點p。
    	3) (p = tab[i = (n - 1) & hash]) == null 判斷結(jié)點位置是否等于null,如果為null,則執(zhí)行代碼:tab[i] = newNode(hash, key, value, null);根據(jù)鍵值對創(chuàng)建新的結(jié)點放入該位置的桶中。
        小結(jié):如果當前桶沒有哈希碰撞沖突,則直接把鍵值對插入空間位置。
    */ 
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 創(chuàng)建一個新的結(jié)點存入到桶中
        tab[i] = newNode(hash, key, value, null);
    else {
         // 執(zhí)行else說明tab[i]不等于null,表示這個位置已經(jīng)有值了
        Node<K,V> e; K k;
        /*
        	比較桶中第一個元素(數(shù)組中的結(jié)點)的hash值和key是否相等
        	1)p.hash == hash :p.hash表示原來存在數(shù)據(jù)的hash值  hash表示后添加數(shù)據(jù)的hash值 比較兩個hash值是否相等。
                 說明:p表示tab[i],即 newNode(hash, key, value, null)方法返回的Node對象。
                    Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
                        return new Node<>(hash, key, value, next);
                    }
                    而在Node類中具有成員變量hash用來記錄著之前數(shù)據(jù)的hash值的。
             2)(k = p.key) == key :p.key獲取原來數(shù)據(jù)的key賦值給k  key 表示后添加數(shù)據(jù)的key比較兩個key的地址值是否相等。
             3)key != null && key.equals(k):能夠執(zhí)行到這里說明兩個key的地址值不相等,那么先判斷后添加的key是否等于null,如果不等于null再調(diào)用equals方法判斷兩個key的內(nèi)容是否相等。
        */
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                /*
                	說明:兩個元素哈希值相等,并且key的值也相等,
                	將舊的元素整體對象賦值給e,用e來記錄
                */ 
                e = p;
        // hash值不相等或者key不相等;判斷p是否為紅黑樹結(jié)點
        else if (p instanceof TreeNode)
            // 放入樹中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 說明是鏈表結(jié)點
        else {
            /*
            	1)如果是鏈表的話需要遍歷到最后結(jié)點然后插入
            	2)采用循環(huán)遍歷的方式,判斷鏈表中是否有重復的key
            */
            for (int binCount = 0; ; ++binCount) {
                /*
                	1)e = p.next 獲取p的下一個元素賦值給e。
                	2)(e = p.next) == null 判斷p.next是否等于null,等于null,說明p沒有下一個元素,那么此時到達了鏈表的尾部,還沒有找到重復的key,則說明HashMap沒有包含該鍵,將該鍵值對插入鏈表中。
                */
                if ((e = p.next) == null) {
                    /*
                    	1)創(chuàng)建一個新的結(jié)點插入到尾部
                    	 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);
                         }
                         注意第四個參數(shù)next是null,因為當前元素插入到鏈表末尾了,那么下一個結(jié)點肯定是null。
                         2)這種添加方式也滿足鏈表數(shù)據(jù)結(jié)構(gòu)的特點,每次向后添加新的元素。
                    */
                    p.next = newNode(hash, key, value, null);
                    /*
                    	1)結(jié)點添加完成之后判斷此時結(jié)點個數(shù)是否大于TREEIFY_THRESHOLD臨界值8,如果大于則將鏈表轉(zhuǎn)換為紅黑樹。
                    	2)int binCount = 0 :表示for循環(huán)的初始化值。從0開始計數(shù)。記錄著遍歷結(jié)點的個數(shù)。值是0表示第一個結(jié)點,1表示第二個結(jié)點。。。。7表示第八個結(jié)點,加上數(shù)組中的的一個元素,元素個數(shù)是9。
                    	TREEIFY_THRESHOLD - 1 --》8 - 1 ---》7
                    	如果binCount的值是7(加上數(shù)組中的的一個元素,元素個數(shù)是9)
                    	TREEIFY_THRESHOLD - 1也是7,此時轉(zhuǎn)換紅黑樹。
                    */
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        // 轉(zhuǎn)換為紅黑樹
                        treeifyBin(tab, hash);
                    // 跳出循環(huán)
                    break;
                }
                /*
                	執(zhí)行到這里說明e = p.next 不是null,不是最后一個元素。繼續(xù)判斷鏈表中結(jié)點的key值與插入的元素的key值是否相等。
                */
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循環(huán)
                    /*
                		要添加的元素和鏈表中的存在的元素的key相等了,則跳出for循環(huán)。不用再繼續(xù)比較了
                		直接執(zhí)行下面的if語句去替換去 if (e != null) 
                	*/
                    break;
                /*
                	說明新添加的元素和當前結(jié)點不相等,繼續(xù)查找下一個結(jié)點。
                	用于遍歷桶中的鏈表,與前面的e = p.next組合,可以遍歷鏈表
                */
                p = e;
            }
        }
        /*
        	表示在桶中找到key值、hash值與插入元素相等的結(jié)點
        	也就是說通過上面的操作找到了重復的鍵,所以這里就是把該鍵的值變?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;
            // 訪問后回調(diào)
            afterNodeAccess(e);
            // 返回舊值
            return oldValue;
        }
    }
    // 修改記錄次數(shù)
    ++modCount;
    // 判斷實際大小是否大于threshold閾值,如果超過則擴容
    if (++size > threshold)
        resize();
    // 插入后回調(diào)
    afterNodeInsertion(evict);
    return null;
}

(1)計算key的hash值;

(2)如果桶(數(shù)組)數(shù)量為0,則初始化桶;

(3)如果key所在的桶沒有元素,則直接插入;

(4)如果key所在的桶中的第一個元素的key與待插入的key相同,說明找到了元素,轉(zhuǎn)后續(xù)流程(9)處理;

(5)如果第一個元素是樹節(jié)點,則調(diào)用樹節(jié)點的putTreeVal()尋找元素或插入樹節(jié)點;

(6)如果不是以上三種情況,則遍歷桶對應(yīng)的鏈表查找key是否存在于鏈表中;

(7)如果找到了對應(yīng)key的元素,則轉(zhuǎn)后續(xù)流程(9)處理;

(8)如果沒找到對應(yīng)key的元素,則在鏈表最后插入一個新節(jié)點并判斷是否需要樹化;

(9)如果找到了對應(yīng)key的元素,則判斷是否需要替換舊值,并直接返回舊值;

(10)如果插入了元素,則數(shù)量加1并判斷是否需要擴容;

7.2 擴容方法 resize()

擴容機制:

1.什么時候才需要擴容?

當 HashMap 中的元素個數(shù)超過數(shù)組大小(數(shù)組長度)*loadFactor(負載因子)時,就會進行數(shù)組擴容,loadFactor 的默認值是 0.75。

2.HashMap 的擴容是什么?

進行擴容,會伴隨著一次重新 hash 分配,并且會遍歷 hash 表中所有的元素,是非常耗時的。在編寫程序中,要盡量避免 resize。

HashMap 在進行擴容時,使用的 rehash 方式非常巧妙,因為每次擴容都是翻倍,與原來計算的 (n - 1) & hash 的結(jié)果相比,只是多了一個 bit 位,所以結(jié)點要么就在原來的位置,要么就被分配到 “原位置 + 舊容量” 這個位置。

例如我們從 16 擴展為 32 時,具體的變化如下所示:

擴容

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

hash

說明:

5 是假設(shè)計算出來的原來的索引。這樣就驗證了上述所描述的:擴容之后所以結(jié)點要么就在原來的位置,要么就被分配到 “原位置 + 舊容量” 這個位置。

因此,我們在擴充 HashMap 的時候,不需要重新計算 hash,只需要看看原來的 hash 值新增的那個 bit 是 1 還是 0 就可以了,是 0 的話索引沒變,是 1 的話索引變成 “原位置 + 舊容量” 。可以看看下圖為 16 擴充為 32 的 resize 示意圖:

擴容

正是因為這樣巧妙的 rehash 方式,既省去了重新計算 hash 值的時間,而且同時,由于新增的 1bit 是 0 還是 1 可以認為是隨機的,在 resize 的過程中保證了 rehash 之后每個桶上的結(jié)點數(shù)一定小于等于原來桶上的結(jié)點數(shù),保證了 rehash 之后不會出現(xiàn)更嚴重的 hash 沖突,均勻的把之前的沖突的結(jié)點分散到新的桶中了。

源碼 resize 方法的解讀

下面是代碼的具體實現(xiàn):

/**
 * 為什么需要擴容呢?
 * 為了解決哈希沖突導致的鏈化影響查詢效率問題,擴容會緩解該問題
 */
final Node<K,V>[] resize() {
    // oldTab:表示擴容前的哈希表數(shù)組
    Node<K,V>[] oldTab = table;
    // oldCap:表示擴容之前table數(shù)組長度
    // 如果當前哈希表數(shù)組等于null 長度返回0,否則返回當前哈希表數(shù)組的長度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // oldThr:表示擴容之前的閥值(觸發(fā)本次擴容的閾值) 默認是12(16*0.75)
    int oldThr = threshold;
    // newCap:擴容之后的table散列表數(shù)組長度
    // newThr: 擴容之后,下次再出發(fā)擴容的條件(新的擴容閾值)
    int newCap, newThr = 0;
    // 如果老的哈希表數(shù)組長度oldCap > 0
    // 如果該條件成立,說明hashMap 中的散列表數(shù)組已經(jīng)初始化過了,是一次正常擴容
    // 開始計算擴容后的大小
    if (oldCap > 0) {
        // 擴容之前的table數(shù)組大小已經(jīng)達到 最大閾值后,則不再擴容
        // 且設(shè)置擴容條件為:int的最大值
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 修改閾值為int的最大值
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 擴容之前的table數(shù)組大小沒超過最大值,則擴充為原來的2倍
        // (newCap = oldCap << 1) < MAXIMUM_CAPACITY 擴大到2倍之后容量要小于最大容量
        // oldCap >= DEFAULT_INITIAL_CAPACITY 原哈希表數(shù)組長度大于等于數(shù)組初始化長度16
        // 如果oldCap 小于默認初始容量16,比如傳入的默認容量為8,則不執(zhí)行下面代碼
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 新的擴容閾值擴大一倍
            newThr = oldThr << 1; // double threshold
    }
    // 如果老的哈希表數(shù)組長度oldCap == 0
    // 說明hashMap中的散列表還沒有初始化,這時候是null
    // 如果老閾值oldThr大于0 直接賦值
    /*
    	以下三種情況會直接進入該判斷:(即,這時候oldThr擴容閾值已存在)
    	1.new HashMap(initCap,loadFactor);
    	2.new HashMap(initCap);
    	3.new HashMap(Map);// 這個傳入的map中已經(jīng)有數(shù)據(jù)
    */
    else if (oldThr > 0) // 老閾值賦值給新的數(shù)組長度
        newCap = oldThr;
    // 如果老的哈希表數(shù)組長度oldCap == 0
    // 說明hashMap中的散列表還沒有初始化,這時候是null
    // 此時,老擴容閾值oldThr == 0
    else { // 直接使用默認值
        newCap = DEFAULT_INITIAL_CAPACITY;//16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//12
    }
    // 如果執(zhí)行到這個位置新的擴容閾值newThr還沒有得到賦值,則
    // 需要計算新的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ù)組長度---> 32
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 說明:hashMap本次擴容之前,table不為null
    if (oldTab != null) {
        // 把每個bucket桶的數(shù)據(jù)都移動到新的散列表中
        // 遍歷舊的哈希表的每個桶,重新計算桶里元素的新位置
        for (int j = 0; j < oldCap; ++j) {
            // 當前node節(jié)點
            Node<K,V> e;
            // 說明:此時的當前桶位中有數(shù)據(jù),但是數(shù)據(jù)具體是 
            // 1.單個數(shù)據(jù) 、 2.還是鏈表 、 3.還是紅黑樹 并不能確定
            if ((e = oldTab[j]) != null) {
                // 原來的數(shù)據(jù)賦值為null 便于GC回收
                oldTab[j] = null;
                // 第一種情況:判斷數(shù)組是否有下一個引用(是否是單個數(shù)據(jù))
                if (e.next == null)
                    // 沒有下一個引用,說明不是鏈表,
                    // 當前桶上只有單個數(shù)據(jù)的鍵值對,
                    // 可以將數(shù)據(jù)直接放入新的散列表中
                    // e.hash & (newCap - 1) 尋址公式得到的索引結(jié)果有兩種:
                    // 1.和原來舊散列表中的索引位置相同,
                    // 2.原來舊散列表中的索引位置i + 舊容量oldCap
                    newTab[e.hash & (newCap - 1)] = e;
                //第二種情況:桶位已經(jīng)形成紅黑樹
                else if (e instanceof TreeNode)
                    // 說明是紅黑樹來處理沖突的,則調(diào)用相關(guān)方法把樹分開
                    // 紅黑樹這塊,我會單獨寫一篇博客給大家詳細分析一下
                    // 紅黑樹相關(guān)可以先跳過
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                // 第三種情況:桶位已經(jīng)形成鏈表
                else { // 采用鏈表處理沖突
                    // 低位鏈表:
                    // 擴容之后數(shù)組的下標位置,與當前數(shù)組的下標位置一致 時使用
                    Node<K,V> loHead = null, loTail = null;
                    // 高位鏈表:擴容之后數(shù)組的下標位置等于
                    // 當前數(shù)組下標位置 + 擴容之前數(shù)組的長度oldCap 時使用
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 通過上述講解的原理來計算結(jié)點的新位置
                    do {
                        // 原索引
                        next = e.next;
                     	// 這里來判斷如果等于true 
                        // e這個結(jié)點在resize之后不需要移動位置
                        // 舉例:
                        // 假如hash1     -> ...... 0 1111
                        // 假如oldCap=16 -> ...... 1 0000
                        // e.hash & oldCap 結(jié)果為0,則
                        // 擴容之后數(shù)組的下標位置j,與當前數(shù)組的下標位置一致
                        // 使用低位鏈表
                        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,則
                        // 擴容之后數(shù)組的下標位置為:
                        // 當前數(shù)組下標位置j + 擴容之前數(shù)組的長度oldCap
                        // 使用高位鏈表
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 將低位鏈表放到bucket桶里
                    if (loTail != null) {
                        loTail.next = null;
                        // 索引位置=當前數(shù)組下標位置j
                        newTab[j] = loHead;
                    }
                    // 將高位鏈表放到bucket里
                    if (hiTail != null) {
                        hiTail.next = null;
                        // 索引位置=當前數(shù)組下標位置j + 擴容之前數(shù)組的長度oldCap
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    // 返回新散列表
    return newTab;
}

(1)如果使用是默認構(gòu)造方法,則第一次插入元素時初始化為默認值,容量為16,擴容門檻為12;

(2)如果使用的是非默認構(gòu)造方法,則第一次插入元素時初始化容量等于擴容門檻,擴容門檻在構(gòu)造方法里等于傳入容量向上最近的2的n次方;

(3)如果舊容量大于0,則新容量等于舊容量的2倍,但不超過最大容量2的30次方,新擴容門檻為舊擴容門檻的2倍;

(4)創(chuàng)建一個新容量的桶;

(5)搬移元素,原鏈表分化成兩個鏈表,低位鏈表存儲在原來桶的位置,高位鏈表搬移到原來桶的位置加舊容量的位置;

7.3 刪除方法 remove()

刪除方法就是首先先找到元素的位置,如果是鏈表就遍歷鏈表找到元素之后刪除。如果是用紅黑樹就遍歷樹然后找到之后做刪除,樹小于 6 的時候要轉(zhuǎn)鏈表。

刪除 remove() 方法

// remove方法的具體實現(xiàn)在removeNode方法中,所以我們重點看下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 當根據(jù) key和value 刪除的時候該參數(shù)為true
    // movable 可以先不用考慮這個參數(shù) 
    // tab:引用當前haashMap中的散列表
    // p:當前node元素
    // n:當前散列表數(shù)組長度
    // index:表示尋址結(jié)果
    Node<K,V>[] tab; Node<K,V> p; int n, index;
	// 根據(jù)hash找到位置 
	// 如果當前key映射到的桶不為空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        // 進入這個if判斷內(nèi)部,說明桶位是有數(shù)據(jù)的,需要進行查詢操作,并且執(zhí)行刪除
        // node:通過查找得到的要刪除的元素
        // e:表示當前node的下一個元素
        // k,v 鍵 值
        Node<K,V> node = null, e; K k; V v;
        // 第一種情況:當前桶位中的元素 即為我們要刪除的元素
        // 如果桶上的結(jié)點就是要找的key,則將node指向該結(jié)點
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        // 如果桶位中的頭一個元素不是我們要找的元素,且桶位中的e = p.next不為null
        // 說明該桶位中的節(jié)點存在下一個節(jié)點
        else if ((e = p.next) != null) {
            // 說明:當前桶位,要么是 鏈表,要么是 紅黑樹
            // 第二種情況:判斷桶位中是否已經(jīng)形成了紅黑樹
            if (p instanceof TreeNode)
                // 說明是以紅黑樹來處理的沖突,則獲取紅黑樹要刪除的結(jié)點
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            // 第三種情況:桶位中已經(jīng)形成鏈表
            else {
                // 判斷是否以鏈表方式處理hash沖突
                // 是的話則通過遍歷鏈表來尋找要刪除的結(jié)點
                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和要刪除的是否匹配
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            // 第一種情況:如果桶位中是紅黑樹,通過調(diào)用紅黑樹的方法來刪除結(jié)點
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            // 第二種情況:如果桶位中是鏈表
            else if (node == p)
                // 鏈表刪除
                tab[index] = node.next;
            // 如果桶位中
            else
                // 第三種情況:將當前元素p的下一個元素設(shè)置為 要刪除元素的 下一個元素
                p.next = node.next;
            // 記錄修改次數(shù)
            ++modCount;
            // 變動的數(shù)量
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

7.4 查找元素方法 get()

查找方法,通過元素的 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:引用當前hashMap的散列表
    // first:桶位中的頭元素
    // e:臨時node元素
    // n:table數(shù)組的長度
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 如果哈希表不為空,并且key對應(yīng)的桶不為空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        /* 
        	判斷數(shù)組元素是否相等
        	根據(jù)索引的位置檢查第一個元素
        	注意:總是檢查第一個元素
        */
        // 第一種情況:定位出來的桶位元素 就是我們要get的數(shù)據(jù)
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 桶位第一個元素不是我們要找的目標元素,且first.next不為null
        // 說明當前桶位不止一個元素,可能是鏈表,也可能是紅黑樹
        if ((e = first.next) != null) {
            // 第二種情況:桶位已經(jīng)升級成了紅黑樹
            // 判斷是否是紅黑樹,是的話調(diào)用紅黑樹中的getTreeNode方法獲取結(jié)點
            if (first instanceof TreeNode)
                // 調(diào)用與樹相關(guān)的方法得到目標元素
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 第三種情況:桶位已經(jīng)形成鏈表
            do {
                // 不是紅黑樹的話,那就是鏈表結(jié)構(gòu)了
                // 通過循環(huán)的方法判斷鏈表中是否存在該key
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    // 如果沒找到返回null
    return null;
}

小結(jié)

get 方法實現(xiàn)的步驟:

a. 通過 hash 值獲取該 key 映射到的桶

b. 桶上的 key 就是要查找的 key,則直接找到并返回

c. 桶上的 key 不是要找的 key,則查看后續(xù)的結(jié)點:

如果后續(xù)結(jié)點是紅黑樹結(jié)點,通過調(diào)用紅黑樹的方法根據(jù) key 獲取 value

如果后續(xù)結(jié)點是鏈表結(jié)點,則通過循環(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());
}

通過 get 方式(不建議使用)

Set<String> keySet = map.keySet();
for (String str : keySet) {
	System.out.println(str + "---" + map.get(str));
}

說明

根據(jù)阿里開發(fā)手冊,不建議使用這種方式,因為迭代兩次。keySet 獲取 Iterator一次,還有通過 get 又迭代一次,降低性能。

jdk8 以后使用 Map 接口中的默認方法:

default void forEach(BiConsumer<? super K,? super V> action) 
// BiConsumer接口中的方法:
	void accept(T t, U u) 對給定的參數(shù)執(zhí)行此操作。  
		參數(shù) 
            t - 第一個輸入?yún)?shù) 
            u - 第二個輸入?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ù)組 + 鏈表 + 紅黑樹)的存儲結(jié)構(gòu);

(2)HashMap的默認初始容量為16(1<<4),默認裝載因子為0.75f,容量總是2的n次方;

(3)HashMap擴容時每次容量變?yōu)樵瓉淼膬杀叮?/p>

(4)當桶的數(shù)量小于64時不會進行樹化,只會擴容;

(5)當桶的數(shù)量大于64且單個桶中元素的數(shù)量大于8時,進行樹化;

(6)當單個桶中元素數(shù)量小于6時,進行反樹化;

(7)HashMap是非線程安全的容器;

(8)HashMap查找添加元素的時間復雜度都為O(1);

這篇文章就到這里了,也希望大家多多關(guān)注腳本之家的其他內(nèi)容!

相關(guān)文章

  • Spring源碼之事件監(jiān)聽機制詳解(@EventListener實現(xiàn)方式)

    Spring源碼之事件監(jiān)聽機制詳解(@EventListener實現(xiàn)方式)

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

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

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

    JAVA ArrayList詳細介紹(示例)

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

    Mybatis的parameterType造成線程阻塞問題分析

    這篇文章主要詳細分析了Mybatis的parameterType造成線程阻塞問題,文中有詳細的解決方法,及相關(guān)的代碼示例,具有一定的參考價值,感興趣的朋友可以借鑒閱讀
    2023-06-06
  • 使用@DS輕松解決動態(tài)數(shù)據(jù)源的問題

    使用@DS輕松解決動態(tài)數(shù)據(jù)源的問題

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

    Springboot訪問templates html頁面過程詳解

    這篇文章主要介紹了Springboot訪問templates html頁面過程詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2020-05-05
  • SpringBoot實現(xiàn)緩存預熱的幾種常用方案

    SpringBoot實現(xiàn)緩存預熱的幾種常用方案

    緩存預熱是指在 Spring Boot 項目啟動時,預先將數(shù)據(jù)加載到緩存系統(tǒng)(如 Redis)中的一種機制,本文給大家介紹了SpringBoot實現(xiàn)緩存預熱的幾種常用方案,并通過代碼示例講解的非常詳細,需要的朋友可以參考下
    2024-02-02
  • idea顯示properties文件中文亂碼的解決方法

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

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

    如何使用SpringBoot集成Kafka實現(xiàn)用戶數(shù)據(jù)變更后發(fā)送消息

    Spring Boot集成Kafka實現(xiàn)用戶數(shù)據(jù)變更后,向其他廠商發(fā)送消息,我們需要考慮配置Kafka連接、創(chuàng)建Kafka Producer發(fā)送消息、監(jiān)聽用戶數(shù)據(jù)變更事件,并將事件轉(zhuǎn)發(fā)到Kafka,本文分步驟給大家講解使用SpringBoot集成Kafka實現(xiàn)用戶數(shù)據(jù)變更后發(fā)送消息,感興趣的朋友一起看看吧
    2024-07-07
  • java三個環(huán)境變量配置簡單教程

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

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

最新評論