1.HashMap集合簡介
HashMap基於哈希表的Map接口實現,是以key-value存儲形式存在,即主要用來存放鍵值對。HashMap 的實現不是同步的,這意味着它不是線程安全的。它的key、value都可以為null。此外,HashMap中的映射不是有序的。
JDK1.8 之前 HashMap 由 數組+鏈表 組成的,數組是 HashMap 的主體,鏈表則是主要為了解決哈希沖突(兩個對象調用的hashCode方法計算的哈希碼值一致導致計算的數組索引值相同)而存在的(“拉鏈法”解決沖突).JDK1.8 以后在解決哈希沖突時有了較大的變化,當鏈表長度大於閾值(或者紅黑樹的邊界值,默認為 8)並且當前數組的長度大於64時,此時此索引位置上的所有數據改為使用紅黑樹存儲。
補充:將鏈表轉換成紅黑樹前會判斷,即使閾值大於8,但是數組長度小於64,此時並不會將鏈表變為紅黑樹。而是選擇進行數組擴容。
這樣做的目的是因為數組比較小,盡量避開紅黑樹結構,這種情況下變為紅黑樹結構,反而會降低效率,因為紅黑樹需要進行左旋,右旋,變色這些操作來保持平衡 。同時數組長度小於64時,搜索時間相對要快些。所以綜上所述為了提高性能和減少搜索時間,底層在閾值大於8並且數組長度大於64時,鏈表才轉換為紅黑樹。具體可以參考 treeifyBin
方法。
當然雖然增了紅黑樹作為底層數據結構,結構變得復雜了,但是閾值大於8並且數組長度大於64時,鏈表轉換為紅黑樹時,效率也變的更高效。
小結:
特點:
1.存取無序的
2.鍵和值位置都可以是null,但是鍵位置只能是一個null
3.鍵位置是唯一的,底層的數據結構控制鍵的
4.jdk1.8前數據結構是:鏈表 + 數組 jdk1.8之后是 : 鏈表 + 數組 + 紅黑樹
5.閾值(邊界值) > 8 並且數組長度大於64,才將鏈表轉換為紅黑樹,變為紅黑樹的目的是為了高效的查詢。
2.HashMap集合底層的數據結構
2.1數據結構概念
數據結構是計算機存儲、組織數據的方式。數據結構是指相互之間存在一種或多種特定關系的數據元素的集合。通常情況下,精心選擇的數據結構可以帶來更高的運行或者存儲效率。數據結構往往同高效的檢索算法和索引技術有關。
數據結構:就是存儲數據的一種方式。ArrayList LinkedList
在JDK1.8 之前 HashMap 由 數組+鏈表 數據結構組成的。
在JDK1.8 之后 HashMap 由 數組+鏈表 +紅黑樹數據結構組成的。
2.2HashMap底層的數據結構存儲數據的過程
存儲過程如下所示:
使用的代碼:
public class Demo01 { public static void main(String[] args) { HashMap<String, Integer> map = new HashMap<>(); map.put("劉德華", 53); map.put("柳岩", 35); map.put("張學友", 55); map.put("郭富城", 52); map.put("黎明", 51); map.put("林青霞", 55); map.put("劉德華", 50); } }
2.面試題:當兩個對象的hashCode相等時會怎么樣?
會產生哈希碰撞,此時會調用對象的equals方法比較key值,若key值內容相同則替換舊的value.不然連接到鏈表后面,鏈表長度超過閾值8就轉換為紅黑樹存儲。
3.面試題:何時發生哈希碰撞和什么是哈希碰撞,如何解決哈希碰撞?
只要兩個元素的key計算的哈希碼值相同就會發生哈希碰撞。jdk8前使用鏈表解決哈希碰撞。jdk8之后使用鏈表+紅黑樹解決哈希碰撞。
4.面試題:如果兩個鍵的hashcode相同,如何存儲鍵值對?
hashcode相同,通過equals比較內容是否相同。
相同:則新的value覆蓋之前的value
不相同:則將新的鍵值對添加到哈希表中
5.在不斷的添加數據的過程中,會涉及到擴容問題,當超出臨界值(且要存放的位置非空)時,擴容。默認的擴容方式:擴容為原來容量的2倍,並將原有的數據復制過來。
但是這樣的話問題來了,傳統hashMap的缺點,1.8為什么引入紅黑樹?這樣結構的話不是更麻煩了嗎,為何閾值大於8換成紅黑樹?
JDK 1.8 以前 HashMap 的實現是 數組+鏈表,即使哈希函數取得再好,也很難達到元素百分百均勻分布。當 HashMap 中有大量的元素都存放到同一個桶中時,這個桶下有一條長長的鏈表,這個時候 HashMap 就相當於一個單鏈表,假如單鏈表有 n 個元素,遍歷的時間復雜度就是 O(n),完全失去了它的優勢。針對這種情況,JDK 1.8 中引入了 紅黑樹(查找時間復雜度為 O(logn))來優化這個問題。 當鏈表長度很小的時候,即使遍歷,速度也非常快,但是當鏈表長度不斷變長,肯定會對查詢性能有一定的影響,所以才需要轉成樹。
7.總結:
上述我們大概闡述了HashMap底層存儲數據的方式。為了方便大家更好的理解,我們結合一個存儲流程圖來進一步說明一下:(jdk8存儲過程)
1.size表示 HashMap中K-V的實時數量 , 注意這個不等於數組的長度 。
2.threshold( 臨界值) =capacity(容量) * loadFactor( 加載因子 )。這個值是當前已占用數組長度的最大值。size超過這個臨界值就重新resize(擴容),擴容后的 HashMap 容量是之前容量的兩倍 。
3.HashMap繼承關系
HashMap繼承關系如下圖所示:
說明:
-
Cloneable 空接口,表示可以克隆。 創建並返回HashMap對象的一個副本。
-
Serializable 序列化接口。屬於標記性接口。HashMap對象可以被序列化和反序列化。
-
AbstractMap 父類提供了Map實現接口。以最大限度地減少實現此接口所需的工作。
補充:通過上述繼承關系我們發現一個很奇怪的現象, 就是HashMap已經繼承了AbstractMap而AbstractMap類實現了Map接口,那為什么HashMap還要在實現Map接口呢?同樣在ArrayList中LinkedList中都是這種結構。
據 java 集合框架的創始人Josh Bloch描述,這樣的寫法是一個失誤。在java集合框架中,類似這樣的寫法很多,最開始寫java集合框架的時候,他認為這樣寫,在某些地方可能是有價值
的,直到他意識到錯了。顯然的,JDK的維護者,后來不認為這個小小的失誤值得去修改,所以就這樣存在下來了。
4.HashMap集合類的成員
1.序列化版本號
private static final long serialVersionUID = 362498820763181265L;
2.集合的初始化容量( 必須是二的n次冪 )
//默認的初始容量是16 -- 1<<4相當於1*2的4次方---1*16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
問題: 為什么必須是2的n次冪?如果輸入值不是2的冪比如10會怎么樣?
HashMap構造方法還可以指定集合的初始化容量大小:
HashMap(int initialCapacity) 構造一個帶指定初始容量和默認加載因子 (0.75) 的空 HashMap。
根據上述講解我們已經知道,當向HashMap中添加一個元素的時候,需要根據key的hash值,去確定其在數組中的具體位置。 HashMap為了存取高效,要盡量較少碰撞,就是要盡量把數據分配均勻,每個鏈表長度大致相同,這個實現就在把數據存到哪個鏈表中的算法。
這個算法實際就是取模,hash%length,計算機中直接求余效率不如位移運算(這點上述已經講解)。所以源碼中做了優化,使用 hash&(length-1),而實際上hash%length等於hash&(length-1)的前提是length是2的n次冪。
為什么這樣能均勻分布減少碰撞呢?2的n次方實際就是1后面n個0,2的n次方-1 實際就是n個1;
舉例:說明:按位與運算:相同的二進制數位上,都是1的時候,結果為1,否則為零。

例如長度為8時候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞; 例如長度length為8時候,8是2的3次冪。二進制是:1000 length-1 二進制運算: 1000 - 1 --------------------- 111 如下所示: hash&(length-1) 3 &(8 - 1)=3 00000011 3 hash & 00000111 7 length-1 --------------------- 00000011-----》3 數組下標 hash&(length-1) 2 & (8 - 1) = 2 00000010 2 hash & 00000111 7 length-1 --------------------- 00000010-----》2 數組下標 說明:上述計算結果是不同位置上,不碰撞;

例如長度為9時候,3&(9-1)=0 2&(9-1)=0 ,都在0上,碰撞了; 例如長度length為9時候,9不是2的n次冪。二進制是:00001001 length-1 二進制運算: 1001 - 1 --------------------- 1000 如下所示: hash&(length-1) 3 &(9 - 1)=0 00000011 3 hash & 00001000 8 length-1 --------------------- 00000000-----》0 數組下標 hash&(length-1) 2 & (9 - 1) = 2 00000010 2 hash & 00001000 8 length-1 --------------------- 00000000-----》0 數組下標 說明:上述計算結果都在0上,碰撞了;
注意: 當然如果不考慮效率直接求余即可(就不需要要求長度必須是2的n次方了)
1.由上面可以看出,當我們根據key的hash確定其在數組的位置時,如果n為2的冪次方,可以保證數據的均勻插入,如果n不是2的冪次方,可能數組的一些位置永遠不會插入數據,浪費數組的空間,加大hash沖突。
2.另一方面,一般我們可能會想通過 % 求余來確定位置,這樣也可以,只不過性能不如 & 運算。而且當n是2的冪次方時:hash & (length - 1) == hash % length
3.因此,HashMap 容量為2次冪的原因,就是為了數據的的均勻分布,減少hash沖突,畢竟hash沖突越大,代表數組中一個鏈的長度越大,這樣的話會降低hashmap的性能
4.如果創建HashMap對象時,輸入的數組長度是10,不是2的冪,HashMap通過一通位移運算和或運算得到的肯定是2的冪次數,並且是離那個數最近的數字。
源代碼如下:

//創建HashMap集合的對象,指定數組長度是10,不是2的冪 HashMap hashMap = new HashMap(10); public HashMap(int initialCapacity) {//initialCapacity=10 this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap(int initialCapacity, float loadFactor) {//initialCapacity=10 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity);//initialCapacity=10 } /** * Returns a power of two size for the given target capacity. */ static final int tableSizeFor(int cap) {//int cap = 10 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(假設是10),由於HashMap的capacity必須都是2的冪,因此這個方法用於找到大於等於initialCapacity(假設是10)的最小的2的冪(initialCapacity如果就是2的冪,則返回的還是這個數)。 下面分析這個算法: 1)、首先,為什么要對cap做減1操作。int n = cap - 1; 這是為了防止,cap已經是2的冪。如果cap已經是2的冪, 又沒有執行這個減1操作,則執行完后面的幾條無符號右移操作之后,返回的capacity將是這個cap的2倍。如果不懂,要看完后面的幾個無符號右移之后再回來看看。 下面看看這幾個無符號右移操作: 2)、如果n這時為0了(經過了cap-1之后),則經過后面的幾次無符號右移依然是0,最后返回的capacity是 1(最后有個n+1的操作)。 這里只討論n不等於0的情況。
3)、注意:|(按位或運算):運算規則:相同的二進制數位上,都是0的時候,結果為0,否則為1。
第一次右移 :
int n = cap - 1;//cap=10 n=9 n |= n >>> 1; 00000000 00000000 00000000 00001001 //9 | 00000000 00000000 00000000 00000100 //9右移之后變為4 ------------------------------------------------- 00000000 00000000 00000000 00001101 //按位異或之后是13
由於n不等於0,則n的二進制表示中總會有一bit為1,這時考慮最高位的1。通過無符號右移1位,則將最高位的1右移了1位,再做或操作,使得n的二進制表示中與最高位的1緊鄰的右邊一位也為1,如:
00000000 00000000 00000000 00001101
n |= n >>> 2;//n通過第一次右移變為了:n=13 00000000 00000000 00000000 00001101 // 13 | 00000000 00000000 00000000 00000011 //13右移之后變為3 ------------------------------------------------- 00000000 00000000 00000000 00001111 //按位異或之后是15
00000000 00000000 00000000 00001111 //按位異或之后是15
n |= n >>> 4;//n通過第一、二次右移變為了:n=15 00000000 00000000 00000000 00001111 // 15 | 00000000 00000000 00000000 00000000 //15右移之后變為0 ------------------------------------------------- 00000000 00000000 00000000 00001111 //按位異或之后是15
注意,得到的這個capacity卻被賦值給了threshold。
this.threshold = tableSizeFor(initialCapacity);//initialCapacity=10
3.默認的負載因子,默認值是0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
4.集合最大容量
//集合最大容量的上限是:2的30次冪 static final int MAXIMUM_CAPACITY = 1 << 30;
5.當鏈表的值超過8則會轉紅黑樹(1.8新增)
//當桶(bucket)上的結點數大於這個值時會轉成紅黑樹
static final int TREEIFY_THRESHOLD = 8;
問題:為什么Map桶中節點個數超過8才轉為紅黑樹?
8這個閾值定義在HashMap中,針對這個成員變量,在源碼的注釋中只說明了8是bin(bin就是bucket(桶))從鏈表轉成樹的閾值,但是並沒有說明為什么是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: 因為樹節點的大小大約是普通節點的兩倍,所以我們只在箱子包含足夠的節點時才使用樹節點(參見TREEIFY_THRESHOLD)。當它們變得太小(由於刪除或調整大小)時,就會被轉換回普通的桶。在使用分布良好的用戶hashcode時,很少使用樹箱。理想情況下,在隨機哈希碼下,箱子中節點的頻率服從泊松分布 (http://en.wikipedia.org/wiki/Poisson_distribution),默認調整閾值為0.75,平均參數約為0.5,盡管由於調整粒度的差異很大。忽略方差,列表大小k的預期出現次數是(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包含足夠多的節點時才會轉成TreeNodes,而是否足夠多就是由TREEIFY_THRESHOLD的值決定的。當bin中節點數變少時,又會轉成普通的bin。並且我們查看源碼的時候發現,鏈表長度達到8就轉成紅黑樹,當長度降到6就轉成普通bin。
這樣就解釋了為什么不是一開始就將其轉換為TreeNodes,而是需要一定節點數才轉為TreeNodes,說白了就是權衡,空間和時間的權衡。
也就是說:選擇8因為符合泊松分布,超過8的時候,概率已經非常小了,所以我們選擇8這個數字。
//當桶(bucket)上的結點數小於這個值時樹轉鏈表 static final int UNTREEIFY_THRESHOLD = 6;
7.當Map里面的數量超過這個值時,表中的桶才能進行樹形化 ,否則桶內元素太多時會擴容,而不是樹形化 為了避免進行擴容、樹形化選擇的沖突,這個值不能小於 4 * TREEIFY_THRESHOLD (8)
//桶中結構轉化為紅黑樹對應的數組長度最小的值 static final int MIN_TREEIFY_CAPACITY = 64;
//存儲元素的數組
transient Node<K,V>[] table;
table在JDK1.8中我們了解到HashMap是由數組加鏈表加紅黑樹來組成的結構其中table就是HashMap中的數組,jdk8之前數組類型是Entry<K,V>類型。從jdk1.8之后是Node<K,V>類型。只是換了個名字,都實現了一樣的接口:Map.Entry<K,V>。負責存儲鍵值對數據的。
9、用來存放緩存
/存放具體元素的集合 transient Set<Map.Entry<K,V>> entrySet;
10、 HashMap中存放元素的個數(重點)
//存放元素的個數,注意這個不等於數組的長度。 transient int size;
size為HashMap中K-V的實時數量,不是數組table的長度。
// 每次擴容和更改map結構的計數器
transient int modCount;
12、 用來調整大小下一個容量的值計算方式為(容量*負載因子)
// 臨界值 當實際大小(容量*負載因子)超過臨界值時,會進行擴容
int threshold;
13、 哈希表的加載因子(重點)
// 加載因子
final float loadFactor;
說明:
1.loadFactor加載因子,是用來衡量 HashMap 滿的程度,表示HashMap的疏密程度,影響hash操作到同一個數組位置的概率,計算HashMap的實時加載因子的方法為:size/capacity,而不是占用桶的數量去除以capacity。capacity 是桶的數量,也就是 table 的長度length。
loadFactor太大導致查找元素效率低,太小導致數組的利用率低,存放的數據會很分散。loadFactor的默認值為0.75f是官方給出的一個比較好的臨界值。
當HashMap里面容納的元素已經達到HashMap數組長度的75%時,表示HashMap太擠了,需要擴容,而擴容這個過程涉及到 rehash、復制數據等操作,非常消耗性能。,所以開發中盡量減少擴容的次數,可以通過創建HashMap集合對象時指定初始容量來盡量避免。
同時在HashMap的構造器中可以定制loadFactor。
構造方法:
HashMap(int initialCapacity, float loadFactor) 構造一個帶指定初始容量和加載因子的空 HashMap。
2.為什么加載因子設置為0.75,初始化臨界值是12?
loadFactor越趨近於1,那么 數組中存放的數據(entry)也就越多,也就越密,也就是會讓鏈表的長度增加,loadFactor越小,也就是趨近於0,數組中存放的數據(entry)也就越少,也就越稀疏。
如果希望鏈表盡可能少些。要提前擴容,有的數組空間有可能一直沒有存儲數據。加載因子盡可能小一些。
舉例:
例如:加載因子是0.4。 那么16*0.4--->6 如果數組中滿6個空間就擴容會造成數組利用率太低了。
加載因子是0.9。 那么16*0.9---->14 那么這樣就會導致鏈表有點多了。導致查找元素效率低。
所以既兼顧數組利用率又考慮鏈表不要太多,經過大量測試0.75是最佳方案。
-
threshold計算公式:capacity(數組長度默認16) * loadFactor(負載因子默認0.75)。這個值是當前已占用數組長度的最大值。當Size>=threshold的時候,那么就要考慮對數組的resize(擴容),也就是說,這個的意思就是 衡量數組是否需要擴增的一個標准。 擴容后的 HashMap 容量是之前容量的兩倍.
4.2構造方法
HashMap 中重要的構造方法,它們分別如下:
1、構造一個空的 HashMap
,默認初始容量(16)和默認負載因子(0.75)。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // 將默認的加載因子0.75賦值給loadFactor,並沒有創建數組
}
2、 構造一個具有指定的初始容量和默認負載因子(0.75) HashMap
。
// 指定“容量大小”的構造函數
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
3、 構造一個具有指定的初始容量和負載因子的 HashMap
。我們來分析一下。

/* 指定“容量大小”和“加載因子”的構造函數 initialCapacity: 指定的容量 loadFactor:指定的加載因子 */ public HashMap(int initialCapacity, float loadFactor) { //判斷初始化容量initialCapacity是否小於0 if (initialCapacity < 0) //如果小於0,則拋出非法的參數異常IllegalArgumentException throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); //判斷初始化容量initialCapacity是否大於集合的最大容量MAXIMUM_CAPACITY-》2的30次冪 if (initialCapacity > MAXIMUM_CAPACITY) //如果超過MAXIMUM_CAPACITY,會將MAXIMUM_CAPACITY賦值給initialCapacity initialCapacity = MAXIMUM_CAPACITY; //判斷負載因子loadFactor是否小於等於0或者是否是一個非數值 if (loadFactor <= 0 || Float.isNaN(loadFactor)) //如果滿足上述其中之一,則拋出非法的參數異常IllegalArgumentException throw new IllegalArgumentException("Illegal load factor: " + loadFactor); //將指定的加載因子賦值給HashMap成員變量的負載因子loadFactor this.loadFactor = loadFactor; /* tableSizeFor(initialCapacity) 判斷指定的初始化容量是否是2的n次冪,如果不是那么會變為比指定初始化容量大的最小的2的n次冪。這點上述已經講解過。 但是注意,在tableSizeFor方法體內部將計算后的數據返回給調用這里了,並且直接賦值給threshold邊界值了。有些人會覺得這里是一個bug,應該這樣書寫: this.threshold = tableSizeFor(initialCapacity) * this.loadFactor; 這樣才符合threshold的意思(當HashMap的size到達threshold這個閾值時會擴容)。 但是,請注意,在jdk8以后的構造方法中,並沒有對table這個成員變量進行初始化,table的初始化被推 遲到了put方法中,在put方法中會對threshold重新計算,put方法的具體實現我們下面會進行講解 */ this.threshold = tableSizeFor(initialCapacity); } 最后調用了tableSizeFor,來看一下方法實現: /** * Returns a power of two size for the given target capacity. 返回比指定初始化容量大的最小的2的n次冪 */ 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次冪,如果不是那么會變為比指定初始化容量大的最小的2的n次冪。這點上述已經講解過。
但是注意,在tableSizeFor方法體內部將計算后的數據返回給調用這里了,並且直接賦值給threshold邊界值了。有些人會覺得這里是一個bug,應該這樣書寫:
this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;
這樣才符合threshold的意思(當HashMap的size到達threshold這個閾值時會擴容)。
但是,請注意,在jdk8以后的構造方法中,並沒有對table這個成員變量進行初始化,table的初始化被推 遲到了put方法中,在put方法中會對threshold重新計算,put方法的具體實現我們下面會進行講解
4、包含另一個“Map”的構造函數
//構造一個映射關系與指定 Map 相同的新 HashMap。
public HashMap(Map<? extends K, ? extends V> m) {
//負載因子loadFactor變為默認的負載因子0.75
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
最后調用了putMapEntries,來看一下方法實現:
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//獲取參數集合的長度
int s = m.size();
if (s > 0)
{
//判斷參數集合的長度是否大於0,說明大於0
if (table == null) // 判斷table是否已經初始化
{ // pre-size
// 未初始化,s為m的實際元素個數
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
// 計算得到的t大於閾值,則初始化閾值
if (t > threshold)
threshold = tableSizeFor(t);
}
// 已初始化,並且m元素個數大於閾值,進行擴容處理
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();
putVal(hash(key), key, value, false, evict);
}
}
}
注意:
float ft = ((float)s / loadFactor) + 1.0F;這一行代碼中為什么要加1.0F ?
s/loadFactor的結果是小數,加1.0F與(int)ft相當於是對小數做一個向上取整以盡可能的保證更大容量,更大的容量能夠減少resize的調用次數。所以 + 1.0F是為了獲取更大的容量。
例如:原來集合的元素個數是6個,那么6/0.75是8,是2的n次冪,那么新的數組大小就是8了。然后原來數組的數據就會存儲到長度是8的新的數組中了,這樣會導致在存儲元素的時候,容量不夠,還得繼續擴容,那么性能降低了,而如果+1呢,數組長度直接變為16了,這樣可以減少數組的擴容。
4.3成員方法
4.3.1增加方法
put方法是比較復雜的,實現步驟大致如下:
1)先通過hash值計算出key映射到哪個桶;
2)如果桶上沒有碰撞沖突,則直接插入;
3)如果出現碰撞沖突了,則需要處理沖突:
a:如果該桶使用紅黑樹處理沖突,則調用紅黑樹的方法插入數據;
b:否則采用傳統的鏈式方法插入。如果鏈的長度達到臨界值,則把鏈轉變為紅黑樹;
4)如果桶中存在重復的鍵,則為該鍵替換新值value;
5)如果size大於閾值threshold,則進行擴容;
具體的方法如下:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
說明:
1)HashMap只提供了put用於添加元素,putVal方法只是給put方法調用的一個方法,並沒有提供給用戶使用。 所以我們重點看putVal方法。
2)我們可以看到在putVal()方法中key在這里執行了一下hash()方法,來看一下Hash方法是如何實現的。
static final int hash(Object key)
{
int h;
/*
1)如果key等於null:
可以看到當key等於null的時候也是有哈希值的,返回的是0.
2)如果key不等於null:
首先計算出key的hashCode賦值給h,然后與h無符號右移16位后的二進制進行按位異或得到最后的hash值
*/
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
從上面可以得知HashMap是支持Key為空的,而HashTable是直接用Key來獲取HashCode所以key為空會拋異常。
{其實上面就已經解釋了為什么HashMap的長度為什么要是2的冪因為HashMap 使用的方法很巧妙,它通過 hash & (table.length -1)來得到該對象的保存位,前面說過 HashMap 底層數組的長度總是2的n次方,這是HashMap在速度上的優化。當 length 總是2的n次方時,hash & (length-1)運算等價於對 length 取模,也就是hash%length,但是&比%具有更高的效率。比如 n % 32 = n & (32 -1)。}
解讀上述hash方法:
我們先研究下key的哈希值是如何計算出來的。key的哈希值是通過上述方法計算出來的。
這個哈希方法首先計算出key的hashCode賦值給h,然后與h無符號右移16位后的二進制進行按位異或得到最后的hash值。計算過程如下所示:
static final int hash(Object key)
{
int h;
/*
1)如果key等於null:
可以看到當key等於null的時候也是有哈希值的,返回的是0.
2)如果key不等於null:
首先計算出key的hashCode賦值給h,然后與h無符號右移16位后的二進制進行按位異或得到最后的hash值
*/
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
在putVal函數中使用到了上述hash函數計算的哈希值:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
。。。。。。。。。。。。。。
if ((p = tab[i = (n - 1) & hash]) == null)//這里的n表示數組長度16
。。。。。。。。。。。。。。
}
計算過程如下所示:
說明:
1)key.hashCode();返回散列值也就是hashcode。假設隨便生成的一個值。
2)n表示數組初始化的長度是16
3)&(按位與運算):運算規則:相同的二進制數位上,都是1的時候,結果為1,否則為零。
4)^(按位異或運算):運算規則:相同的二進制數位上,數字相同,結果為0,不同為1。
簡單來說就是:
-
高16 bit 不變,低16 bit 和高16 bit 做了一個異或(得到的 hashcode 轉化為32位二進制,前16位和后16位低16 bit和高16 bit做了一個異或)
問題:為什么要這樣操作呢?
如果當n即數組長度很小,假設是16的話,那么n-1即為 ---》1111 ,這樣的值和hashCode()直接做按位與操作,實際上只使用了哈希值的后4位。如果當哈希值的高位變化很大,低位變化很小,這樣就很容易造成哈希沖突了,所以這里把高低位都利用起來,從而解決了這個問題。
例如上述:
hashCode()值: 1111 1111 1111 1111 1111 0000 1110 1010
&
n-1即16-1--》15: 。。。。。。。。。。。。。。。。。。。。。。1111
-------------------------------------------------------------------
0000 0000 0000 0000 0000 0000 0000 1010 ----》10作為索引
其實就是將hashCode值作為數組索引,那么如果下個高位hashCode不一致,低位一致的話,就會造成計算的索引還是10,從而造成了哈希沖突了。降低性能。 -
(n-1) & hash = -> 得到下標 (n-1) n表示數組長度16,n-1就是15
-
取余數本質是不斷做除法,把剩余的數減去,運算效率要比位運算低。
現在看putVal()方法,看看它到底做了什么。
主要參數:
-
hash key的hash值
-
key 原始Key
-
value 要存放的值
-
onlyIfAbsent 如果true代表不更改現有的值
-
evict 如果為false表示table為創建狀態
putVal()方法源代碼如下所示:

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

/** * Replaces all linked nodes in bin at index for given hash unless * table is too small, in which case resizes instead. 替換指定哈希表的索引處桶中的所有鏈接節點,除非表太小,否則將修改大小。 Node<K,V>[] tab = tab 數組名 int hash = hash表示哈希值 */ final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; /* 如果當前數組為空或者數組的長度小於進行樹形化的閾值(MIN_TREEIFY_CAPACITY = 64), 就去擴容。而不是將節點變為紅黑樹。 目的:如果數組很小,那么轉換紅黑樹,然后遍歷效率要低一些。這時進行擴容,那么重新計算哈希值 ,鏈表長度有可能就變短了,數據會放到數組中,這樣相對來說效率高一些。 */ if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) //擴容方法 resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { /* 1)執行到這里說明哈希表中的數組長度大於閾值64,開始進行樹形化 2)e = tab[index = (n - 1) & hash]表示將數組中的元素取出賦值給e,e是哈希表中指定位 置桶里的鏈表節點,從第一個開始 */ //hd:紅黑樹的頭結點 tl :紅黑樹的尾結點 TreeNode<K,V> hd = null, tl = null; do { //新創建一個樹的節點,內容和當前鏈表節點e一致 TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) //將新創鍵的p節點賦值給紅黑樹的頭結點 hd = p; else { /* p.prev = tl:將上一個節點p賦值給現在的p的前一個節點 tl.next = p;將現在節點p作為樹的尾結點的下一個節點 */ p.prev = tl; tl.next = p; } tl = p; /* e = e.next 將當前節點的下一個節點賦值給e,如果下一個節點不等於null 則回到上面繼續取出鏈表中節點轉換為紅黑樹 */ } while ((e = e.next) != null); /* 讓桶中的第一個元素即數組中的元素指向新建的紅黑樹的節點,以后這個桶里的元素就是紅黑樹 而不是鏈表數據結構了 */ if ((tab[index] = hd) != null) hd.treeify(tab); } }
小結:上述操作一共做了如下幾件事:
1.根據哈希表中元素個數確定是擴容還是樹形化
2.如果是樹形化遍歷桶中的元素,創建相同個數的樹形節點,復制內容,建立起聯系
3.然后讓桶中的第一個元素指向新創建的樹根節點,替換桶的鏈表內容為樹形化內容
4.3.3擴容方法_resize
4.3.3.1擴容機制
想要了解HashMap的擴容機制你要有這兩個問題
-
1.什么時候才需要擴容
-
2.HashMap的擴容是什么
1.什么時候才需要擴容
當HashMap中的元素個數超過數組大小(數組長度)*loadFactor(負載因子)時,就會進行數組擴容,loadFactor的默認值(DEFAULT_LOAD_FACTOR)是0.75,這是一個折中的取值。也就是說,默認情況下,數組大小為16,那么當HashMap中的元素個數超過16×0.75=12(這個值就是閾值或者邊界值threshold值)的時候,就把數組的大小擴展為2×16=32,即擴大一倍,然后重新計算每個元素在數組中的位置,而這是一個非常耗性能的操作,所以如果我們已經預知HashMap中元素的個數,那么預知元素的個數能夠有效的提高HashMap的性能。
補充:
當HashMap中的其中一個鏈表的對象個數如果達到了8個,此時如果數組長度沒有達到64,那么HashMap會先擴容解決,如果已經達到了64,那么這個鏈表會變成紅黑樹,節點類型由Node變成TreeNode類型。當然,如果映射關系被移除后,下次執行resize方法時判斷樹的節點個數低於6,也會再把樹轉換為鏈表。
2.HashMap的擴容是什么
進行擴容,會伴隨着一次重新hash分配,並且會遍歷hash表中所有的元素,是非常耗時的。在編寫程序中,要盡量避免resize。
HashMap在進行擴容時,使用的rehash方式非常巧妙,因為每次擴容都是翻倍,與原來計算的 (n-1)&hash的結果相比,只是多了一個bit位,所以節點要么就在原來的位置,要么就被分配到"原位置+舊容量"這個位置。
怎么理解呢?例如我們從16擴展為32時,具體的變化如下所示:
因此元素在重新計算hash之后,因為n變為2倍,那么n-1的標記范圍在高位多1bit(紅色),因此新的index就會發生這樣的變化:
說明:5是假設計算出來的原來的索引。這樣就驗證了上述所描述的:擴容之后所以節點要么就在原來的位置,要么就被分配到"原位置+舊容量"這個位置。
因此,我們在擴充HashMap的時候,不需要重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就可以了,是0的話索引沒變,是1的話索引變成“原索引+oldCap(原位置+舊容量)”。可以看看下圖為16擴充為32的resize示意圖:
正是因為這樣巧妙的rehash方式,既省去了重新計算hash值的時間,而且同時,由於新增的1bit是0還是1可以認為是隨機的,在resize的過程中保證了rehash之后每個桶上的節點數一定小於等於原來桶上的節點數,保證了rehash之后不會出現更嚴重的hash沖突,均勻的把之前的沖突的節點分散到新的桶中了。
4.3.3.2源碼resize方法的解讀
下面是代碼的具體實現:

final Node<K,V>[] resize() { //得到當前數組 Node<K,V>[] oldTab = table; //如果當前數組等於null長度返回0,否則返回當前數組的長度 int oldCap = (oldTab == null) ? 0 : oldTab.length; //當前閥值點 默認是12(16*0.75) int oldThr = threshold; int newCap, newThr = 0; //如果老的數組長度大於0 //開始計算擴容后的大小 if (oldCap > 0) { // 超過最大值就不再擴充了,就只好隨你碰撞去吧 if (oldCap >= MAXIMUM_CAPACITY) { //修改閾值為int的最大值 threshold = Integer.MAX_VALUE; return oldTab; } /* 沒超過最大值,就擴充為原來的2倍 1)(newCap = oldCap << 1) < MAXIMUM_CAPACITY 擴大到2倍之后容量要小於最大容量 2)oldCap >= DEFAULT_INITIAL_CAPACITY 原數組長度大於等於數組初始化長度16 */ else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) //閾值擴大一倍 newThr = oldThr << 1; // double threshold } //老閾值點大於0 直接賦值 else if (oldThr > 0) // 老閾值賦值給新的數組長度 newCap = oldThr; else {// 直接使用默認值 newCap = DEFAULT_INITIAL_CAPACITY;//16 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 計算新的resize最大上限 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //新的閥值 默認原來是12 乘以2之后變為24 threshold = newThr; //創建新的哈希表 @SuppressWarnings({"rawtypes","unchecked"}) //newCap是新的數組長度--》32 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; //判斷舊數組是否等於空 if (oldTab != null) { // 把每個bucket都移動到新的buckets中 //遍歷舊的哈希表的每個桶,重新計算桶里元素的新位置 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { //原來的數據賦值為null 便於GC回收 oldTab[j] = null; //判斷數組是否有下一個引用 if (e.next == null) //沒有下一個引用,說明不是鏈表,當前桶上只有一個鍵值對,直接插入 newTab[e.hash & (newCap - 1)] = e; //判斷是否是紅黑樹 else if (e instanceof TreeNode) //說明是紅黑樹來處理沖突的,則調用相關方法把樹分開 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // 采用鏈表處理沖突 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; //通過上述講解的原理來計算節點的新位置 do { // 原索引 next = e.next; //這里來判斷如果等於true e這個節點在resize之后不需要移動位置 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } // 原索引+oldCap else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 原索引放到bucket里 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } // 原索引+oldCap放到bucket里 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
4.3.4 刪除方法(remove)
理解了put方法之后,remove方法已經沒什么難度了,所以重復的內容就不再做詳細介紹了。
刪除的話就是首先先找到元素的位置,如果是鏈表就遍歷鏈表找到元素之后刪除。如果是用紅黑樹就遍歷樹然后找到之后做刪除,樹小於6的時候要轉鏈表。
刪除remove方法:
//remove方法的具體實現在removeNode方法中,所以我們重點看下removeNode方法
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
removeNode方法:

final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; //根據hash找到位置 //如果當前key映射到的桶不為空 if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v; //如果桶上的節點就是要找的key,則將node指向該節點 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { //說明節點存在下一個節點 if (p instanceof TreeNode) //說明是以紅黑樹來處理的沖突,則獲取紅黑樹要刪除的節點 node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { //判斷是否以鏈表方式處理hash沖突,是的話則通過遍歷鏈表來尋找要刪除的節點 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)))) { //通過調用紅黑樹的方法來刪除節點 if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); else if (node == p) //鏈表刪除 tab[index] = node.next; else p.next = node.next; //記錄修改次數 ++modCount; //變動的數量 --size; afterNodeRemoval(node); return node; } } return null; }
4.3.5查找元素方法(get)
查找方法,通過元素的Key找到Value。
代碼如下:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
get方法主要調用的是getNode方法,代碼如下:

final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //如果哈希表不為空並且key對應的桶上不為空 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { /* 判斷數組元素是否相等 根據索引的位置檢查第一個元素 注意:總是檢查第一個元素 */ if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; // 如果不是第一個元素,判斷是否有后續節點 if ((e = first.next) != null) { // 判斷是否是紅黑樹,是的話調用紅黑樹中的getTreeNode方法獲取節點 if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { // 不是紅黑樹的話,那就是鏈表結構了,通過循環的方法判斷鏈表中是否存在該key if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
小結:
1.get方法實現的步驟:
1)通過hash值獲取該key映射到的桶
2)桶上的key就是要查找的key,則直接找到並返回
3)桶上的key不是要找的key,則查看后續的節點:
a:如果后續節點是紅黑樹節點,通過調用紅黑樹的方法根據key獲取value
b:如果后續節點是鏈表節點,則通過循環遍歷鏈表根據key獲取value
2.上述紅黑樹節點調用的是getTreeNode方法通過樹形節點的find方法進行查找:

final TreeNode<K,V> getTreeNode(int h, Object k) { return ((parent != null) ? root() : this).find(h, k, null); } final TreeNode<K,V> find(int h, Object k, Class<?> kc) { TreeNode<K,V> p = this; do { int ph, dir; K pk; TreeNode<K,V> pl = p.left, pr = p.right, q; if ((ph = p.hash) > h) p = pl; else if (ph < h) p = pr; else if ((pk = p.key) == k || (k != null && k.equals(pk))) return p;//找到之后直接返回 else if (pl == null) p = pr; else if (pr == null) p = pl; else if ((kc != null || (kc = comparableClassFor(k)) != null) && (dir = compareComparables(kc, k, pk)) != 0) p = (dir < 0) ? pl : pr; //遞歸查找 else if ((q = pr.find(h, k, kc)) != null) return q; else p = pl; } while (p != null); return null; }
3.查找紅黑樹,由於之前添加時已經保證這個樹是有序的了,因此查找時基本就是折半查找,效率更高。
4.這里和插入時一樣,如果對比節點的哈希值和要查找的哈希值相等,就會判斷key是否相等,相等就直接返回。不相等就從子樹中遞歸查找。
5.
若為鏈表,則在鏈表中通過key.equals(k)查找,O(n)。
1、分別遍歷Key和Values
2、使用Iterator迭代器迭代
3、通過get方式(不建議使用)
說明:根據阿里開發手冊,不建議使用這種方式,因為迭代兩次。keySet獲取Iterator一次,還有通過get又迭代一次。降低性能。
4.jdk8以后使用Map接口中的默認方法:
default void forEach(BiConsumer<? super K,? super V> action)
BiConsumer接口中的方法:
void accept•(T t, U u) 對給定的參數執行此操作。
參數
t - 第一個輸入參數
u - 第二個輸入參數
遍歷代碼:
public class Demo02 {
public static void main(String[] args) {
HashMap<String,String> m1 = new HashMap();
m1.put("001", "zhangsan");
m1.put("002", "lisi");
m1.forEach((key,value)->{
System.out.println(key+"---"+value);
});
}
}
5.如何設計多個非重復的鍵值對要存儲HashMap的初始化?
5.1HashMap的初始化問題描述
如果我們確切的知道我們有多少鍵值對需要存儲,那么我們在初始化HashMap的時候就應該指定它的容量,以防止HashMap自動擴容,影響使用效率。
默認情況下HashMap的容量是16,但是,如果用戶通過構造函數指定了一個數字作為容量,那么Hash會選擇大於該數字的第一個2的冪作為容量。(3->4、7->8、9->16) .這點我們在上述已經進行過講解。
《阿里巴巴Java開發手冊》中建議我們設置HashMap的初始化容量。
那么,為什么要這么建議?你有想過沒有。
當然,以上建議也是有理論支撐的。我們上面介紹過,HashMap的擴容機制,就是當達到擴容條件時會進行擴容。HashMap的擴容條件就是當HashMap中的元素個數(size)超過臨界值(threshold)時就會自動擴容。在HashMap中,threshold = loadFactor * capacity。
所以,如果我們沒有設置初始容量大小,隨着元素的不斷增加,HashMap會有可能發生多次擴容,而HashMap中的擴容機制決定了每次擴容都需要重建hash表,是非常影響性能的。
但是設置初始化容量,設置的數值不同也會影響性能,那么當我們已知HashMap中即將存放的KV個數的時候,容量設置成多少為好呢?
5.2HashMap中容量的初始化
當我們使用HashMap(int initialCapacity)來初始化容量的時候,jdk會默認幫我們計算一個相對合理的值當做初始容量。那么,是不是我們只需要把已知的HashMap中即將存放的元素個數直接傳給initialCapacity就可以了呢?
關於這個值的設置,在《阿里巴巴Java開發手冊》有以下建議:
也就是說,如果我們設置的默認值是7,經過Jdk處理之后,會被設置成8,但是,這個HashMap在元素個數達到 8*0.75 = 6的時候就會進行一次擴容,這明顯是我們不希望見到的。我們應該盡量減少擴容。原因也已經分析過。
如果我們通過initialCapacity/ 0.75F + 1.0F計算,7/0.75 + 1 = 10 ,10經過Jdk處理之后,會被設置成16,這就大大的減少了擴容的幾率。
當HashMap內部維護的哈希表的容量達到75%時(默認情況下),會觸發rehash,而rehash的過程是比較耗費時間的。所以初始化容量要設置成initialCapacity/0.75 + 1的話,可以有效的減少沖突也可以減小誤差。
所以,我可以認為,當我們明確知道HashMap中元素的個數的時候,把默認容量設置成initialCapacity/ 0.75F + 1.0F是一個在性能上相對好的選擇,但是,同時也會犧牲些內存。
我們想要在代碼中創建一個HashMap的時候,如果我們已知這個Map中即將存放的元素個數,給HashMap設置初始容量可以在一定程度上提升效率。
但是,JDK並不會直接拿用戶傳進來的數字當做默認容量,而是會進行一番運算,最終得到一個2的冪。原因也已經分析過。
但是,為了最大程度的避免擴容帶來的性能消耗,我們建議可以把默認容量的數字設置成initialCapacity/ 0.75F + 1.0F。
//桶中結構轉化為紅黑樹對應的數組長度最小的值 static final int MIN_TREEIFY_CAPACITY = 64;