做者:小傅哥
博客:https://bugstack.cnhtml
沉澱、分享、成長,讓本身和他人都能有所收穫!😄
得益於Doug Lea
老爺子的操刀,讓HashMap
成爲使用和麪試最頻繁的API,沒辦法設計的太優秀了!java
HashMap 最先出如今 JDK 1.2中,底層基於散列算法實現。HashMap 容許 null 鍵和 null 值,在計算哈鍵的哈希值時,null 鍵哈希值爲 0。HashMap 並不保證鍵值對的順序,這意味着在進行某些操做後,鍵值對的順序可能會發生變化。另外,須要注意的是,HashMap 是非線程安全類,在多線程環境下可能會存在問題。程序員
HashMap 最先在JDK 1.2中就出現了,底層是基於散列算法實現,隨着幾代的優化更新到目前爲止它的源碼部分已經比較複雜,涉及的知識點也很是多,在JDK 1.8中包括;一、散列表實現
、二、擾動函數
、三、初始化容量
、四、負載因子
、五、擴容元素拆分
、六、鏈表樹化
、七、紅黑樹
、八、插入
、九、查找
、十、刪除
、十一、遍歷
、十二、分段鎖
等等,因涉及的知識點較多因此須要分開講解,本章節咱們會先把目光放在前五項上,也就是關於數據結構的使用上。面試
數據結構相關每每與數學離不開,學習過程當中建議下載相應源碼進行實驗驗證,可能這個過程有點燒腦,但學會後不用死記硬背就能夠理解這部分知識。算法
本章節涉及的源碼和資源在工程,interview-04中,包括;數組
interview-04
工程中能夠經過關注公衆號:bugstack蟲洞棧
,回覆下載進行獲取{回覆下載後打開得到的連接,找到編號ID:19}安全
學習HashMap前,最好的方式是先了解這是一種怎麼樣的數據結構來存放數據。而HashMap通過多個版本的迭代後,乍一看代碼仍是很複雜的。就像你原來只穿個褲衩,如今還有秋褲和風衣。因此咱們先來看看最根本的HashMap是什麼樣,也就是隻穿褲衩是什麼效果,以後再去分析它的源碼。數據結構
問題: 假設咱們有一組7個字符串,須要存放到數組中,但要求在獲取每一個元素的時候時間複雜度是O(1)。也就是說你不能經過循環遍歷的方式進行獲取,而是要定位到數組ID直接獲取相應的元素。多線程
方案: 若是說咱們須要經過ID從數組中獲取元素,那麼就須要把每一個字符串都計算出一個在數組中的位置ID。字符串獲取ID你能想到什麼方式? 一個字符串最直接的獲取跟數字相關的信息就是HashCode,可HashCode的取值範圍太大了[-2147483648, 2147483647]
,不可能直接使用。那麼就須要使用HashCode與數組長度作與運算,獲得一個能夠在數組中出現的位置。若是說有兩個元素獲得一樣的ID,那麼這個數組ID下就存放兩個字符串。函數
以上呢其實就是咱們要把字符串散列到數組中的一個基本思路,接下來咱們就把這個思路用代碼實現出來。
// 初始化一組字符串 List<String> list = new ArrayList<>(); list.add("jlkk"); list.add("lopi"); list.add("小傅哥"); list.add("e4we"); list.add("alpo"); list.add("yhjk"); list.add("plop"); // 定義要存放的數組 String[] tab = new String[8]; // 循環存放 for (String key : list) { int idx = key.hashCode() & (tab.length - 1); // 計算索引位置 System.out.println(String.format("key值=%s Idx=%d", key, idx)); if (null == tab[idx]) { tab[idx] = key; continue; } tab[idx] = tab[idx] + "->" + key; } // 輸出測試結果 System.out.println(JSON.toJSONString(tab));
這段代碼總體看起來也是很是簡單,並無什麼複雜度,主要包括如下內容;
0111
除高位之外都是1的特徵,也是爲了散列。key.hashCode() & (tab.length - 1)
。模擬鏈表的過程
。測試結果
key值=jlkk Idx=2 key值=lopi Idx=4 key值=小傅哥 Idx=7 key值=e4we Idx=5 key值=alpo Idx=2 key值=yhjk Idx=0 key值=plop Idx=5 測試結果:["yhjk",null,"jlkk->alpo",null,"lopi","e4we->plop",null,"小傅哥"]
e4we->plop
。若是上面的測試結果不能在你的頭腦中很好的創建出一個數據結構,那麼能夠看如下這張散列示意圖,方便理解;
以上咱們實現了一個簡單的HashMap,或者說還算不上HashMap,只能算作一個散列數據存放的雛形。但這樣的一個數據結構放在實際使用中,會有哪些問題呢?
以上這些問題能夠概括爲;擾動函數
、初始化容量
、負載因子
、擴容方法
以及鏈表和紅黑樹
轉換的使用等。接下來咱們會逐個問題進行分析。
在HashMap存放元素時候有這樣一段代碼來處理哈希值,這是java 8
的散列值擾動函數,用於優化散列效果;
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
理論上來講字符串的hashCode
是一個int類型值,那能夠直接做爲數組下標了,且不會出現碰撞。可是這個hashCode
的取值範圍是[-2147483648, 2147483647],有將近40億的長度,誰也不能把數組初始化的這麼大,內存也是放不下的。
咱們默認初始化的Map大小是16個長度 DEFAULT_INITIAL_CAPACITY = 1 << 4
,因此獲取的Hash值並不能直接做爲下標使用,須要與數組長度進行取模運算獲得一個下標值,也就是咱們上面作的散列列子。
那麼,hashMap源碼這裏不僅是直接獲取哈希值,還進行了一次擾動計算,(h = key.hashCode()) ^ (h >>> 16)
。把哈希值右移16位,也就正好是本身長度的一半,以後與原哈希值作異或運算,這樣就混合了原哈希值中的高位和低位,增大了隨機性。計算方式以下圖;
從上面的分析能夠看出,擾動函數使用了哈希值的高半區和低半區作異或,混合原始哈希碼的高位和低位,以此來加大低位區的隨機性。
但看不到實驗數據的話,這終究是一段理論,具體這段哈希值真的被增長了隨機性沒有,並不知道。因此這裏咱們要作一個實驗,這個實驗是這樣作;
擾動函數對比方法
public class Disturb { public static int disturbHashIdx(String key, int size) { return (size - 1) & (key.hashCode() ^ (key.hashCode() >>> 16)); } public static int hashIdx(String key, int size) { return (size - 1) & key.hashCode(); } }
disturbHashIdx
擾動函數下,下標值計算hashIdx
非擾動函數下,下標值計算單元測試
// 10萬單詞已經初始化到words中 @Test public void test_disturb() { Map<Integer, Integer> map = new HashMap<>(16); for (String word : words) { // 使用擾動函數 int idx = Disturb.disturbHashIdx(word, 128); // 不使用擾動函數 // int idx = Disturb.hashIdx(word, 128); if (map.containsKey(idx)) { Integer integer = map.get(idx); map.put(idx, ++integer); } else { map.put(idx, 1); } } System.out.println(map.values()); }
以上分別統計兩種函數下的下標值分配,最終將統計結果放到excel中生成圖表。
以上的兩張圖,分別是沒有使用擾動函數和使用擾動函數的,下標分配。實驗數據;
未使用擾動函數
使用擾動函數
接下來咱們討論下一個問題,從咱們模仿HashMap的例子中以及HashMap默認的初始化大小裏,均可以知道,散列數組須要一個2的倍數的長度,由於只有2的倍數在減1的時候,纔會出現01111
這樣的值。
那麼這裏就有一個問題,咱們在初始化HashMap的時候,若是傳一個17個的值new HashMap<>(17);
,它會怎麼處理呢?
在HashMap的初始化中,有這樣一段方法;
public HashMap(int initialCapacity, float loadFactor) { ... this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); }
threshold
,經過方法tableSizeFor
進行計算,是根據初始化來計算的。計算閥值大小的方法;
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; }
那這裏咱們把17這樣一個初始化計算閥值的過程,用圖展現出來,方便理解;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
負載因子是作什麼的?
負載因子,能夠理解成一輛車可承重重量超過某個閥值時,把貨放到新的車上。
那麼在HashMap中,負載因子決定了數據量多少了之後進行擴容。這裏要提到上面作的HashMap例子,咱們準備了7個元素,可是最後還有3個位置空餘,2個位置存放了2個元素。 因此可能即便你數據比數組容量大時也是不必定能正正好好的把數組佔滿的,而是在某些小標位置出現了大量的碰撞,只能在同一個位置用鏈表存放,那麼這樣就失去了Map數組的性能。
因此,要選擇一個合理的大小下進行擴容,默認值0.75就是說當閥值容量佔了3/4s時趕忙擴容,減小Hash碰撞。
同時0.75是一個默認構造值,在建立HashMap也能夠調整,好比你但願用更多的空間換取時間,能夠把負載因子調的更小一些,減小碰撞。
爲何擴容,由於數組長度不足了。那擴容最直接的問題,就是須要把元素拆分到新的數組中。拆分元素的過程當中,原jdk1.7中會須要從新計算哈希值,可是到jdk1.8中已經進行優化,不在須要從新計算,提高了拆分的性能,設計的仍是很是巧妙的。
@Test public void test_hashMap() { List<String> list = new ArrayList<>(); list.add("jlkk"); list.add("lopi"); list.add("jmdw"); list.add("e4we"); list.add("io98"); list.add("nmhg"); list.add("vfg6"); list.add("gfrt"); list.add("alpo"); list.add("vfbh"); list.add("bnhj"); list.add("zuio"); list.add("iu8e"); list.add("yhjk"); list.add("plop"); list.add("dd0p"); for (String key : list) { int hash = key.hashCode() ^ (key.hashCode() >>> 16); System.out.println("字符串:" + key + " \tIdx(16):" + ((16 - 1) & hash) + " \tBit值:" + Integer.toBinaryString(hash) + " - " + Integer.toBinaryString(hash & 16) + " \t\tIdx(32):" + (( System.out.println(Integer.toBinaryString(key.hashCode()) +" "+ Integer.toBinaryString(hash) + " " + Integer.toBinaryString((32 - 1) & hash)); } }
測試結果
字符串:jlkk Idx(16):3 Bit值:1100011101001000010011 - 10000 Idx(32):19 1100011101001000100010 1100011101001000010011 10011 字符串:lopi Idx(16):14 Bit值:1100101100011010001110 - 0 Idx(32):14 1100101100011010111100 1100101100011010001110 1110 字符串:jmdw Idx(16):7 Bit值:1100011101010100100111 - 0 Idx(32):7 1100011101010100010110 1100011101010100100111 111 字符串:e4we Idx(16):3 Bit值:1011101011101101010011 - 10000 Idx(32):19 1011101011101101111101 1011101011101101010011 10011 字符串:io98 Idx(16):4 Bit值:1100010110001011110100 - 10000 Idx(32):20 1100010110001011000101 1100010110001011110100 10100 字符串:nmhg Idx(16):13 Bit值:1100111010011011001101 - 0 Idx(32):13 1100111010011011111110 1100111010011011001101 1101 字符串:vfg6 Idx(16):8 Bit值:1101110010111101101000 - 0 Idx(32):8 1101110010111101011111 1101110010111101101000 1000 字符串:gfrt Idx(16):1 Bit值:1100000101111101010001 - 10000 Idx(32):17 1100000101111101100001 1100000101111101010001 10001 字符串:alpo Idx(16):7 Bit值:1011011011101101000111 - 0 Idx(32):7 1011011011101101101010 1011011011101101000111 111 字符串:vfbh Idx(16):1 Bit值:1101110010111011000001 - 0 Idx(32):1 1101110010111011110110 1101110010111011000001 1 字符串:bnhj Idx(16):0 Bit值:1011100011011001100000 - 0 Idx(32):0 1011100011011001001110 1011100011011001100000 0 字符串:zuio Idx(16):8 Bit值:1110010011100110011000 - 10000 Idx(32):24 1110010011100110100001 1110010011100110011000 11000 字符串:iu8e Idx(16):8 Bit值:1100010111100101101000 - 0 Idx(32):8 1100010111100101011001 1100010111100101101000 1000 字符串:yhjk Idx(16):8 Bit值:1110001001010010101000 - 0 Idx(32):8 1110001001010010010000 1110001001010010101000 1000 字符串:plop Idx(16):9 Bit值:1101001000110011101001 - 0 Idx(32):9 1101001000110011011101 1101001000110011101001 1001 字符串:dd0p Idx(16):14 Bit值:1011101111001011101110 - 0 Idx(32):14 1011101111001011000000 1011101111001011101110 1110
zuio
因計算結果 hash & oldCap
不爲1,則被遷移到下標位置24。一、散列表實現
、二、擾動函數
、三、初始化容量
、四、負載因子
、五、擴容元素拆分
。