日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

图解HashMap(一)

發(fā)布時(shí)間:2023/12/20 编程问答 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 图解HashMap(一) 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

概述

HashMap是日常開發(fā)中經(jīng)常會(huì)用到的一種數(shù)據(jù)結(jié)構(gòu),在介紹HashMap的時(shí)候會(huì)涉及到很多術(shù)語(yǔ),比如時(shí)間復(fù)雜度O、散列(也叫哈希)、散列算法等,這些在大學(xué)課程里都有教過(guò),但是由于某種不可抗力又還給老師了,在深入學(xué)習(xí)HashMap之前先了解HashMap設(shè)計(jì)的思路以及以及一些重要概念,在后續(xù)分析源碼的時(shí)候就能夠有比較清晰的認(rèn)識(shí)。

HashMap是什么

在回答這個(gè)問(wèn)題之前先看個(gè)例子:小明打算從A市搬家到B市,拿了兩個(gè)箱子把自己的物品打包就出發(fā)了。

到了B市之后,他想拿手機(jī)給家里報(bào)個(gè)平安,這時(shí)候問(wèn)題來(lái)了,東西多了他忘記手機(jī)放在哪個(gè)箱子了?小明開始打開1號(hào)箱子找手機(jī),沒(méi)找到;再打開2號(hào)箱子找,找到手機(jī)。當(dāng)只有2個(gè)箱子的時(shí)候,東西又不多的情況下,他可能花個(gè)2分鐘就找到手機(jī)了,假如有20個(gè)箱子,每個(gè)箱子的東西又多又雜,那么花的時(shí)間就多了。小明總結(jié)了下查找耗時(shí)的原因,發(fā)現(xiàn)是因?yàn)檫@些東西放的沒(méi)有規(guī)律,如果他把每個(gè)箱子分個(gè)類別,比如定一個(gè)箱子專門放手機(jī)、電腦等電子設(shè)備,有專門放衣服的箱子等等,那么他找東西花的時(shí)間就可以大大縮短了。

其實(shí)HashMap也是用到這種思路,HashMap作為一種數(shù)據(jù)結(jié)構(gòu),像數(shù)組和鏈表一樣用于常規(guī)的增刪改查,在存數(shù)據(jù)的時(shí)候(put)并不是隨便亂放,而是會(huì)先做一次類似“分類”的操作再存儲(chǔ),一旦“分類”存儲(chǔ)之后,下次取(get)的時(shí)候就可以大大縮短查找的時(shí)間。我們知道數(shù)組在執(zhí)行查、改的效率很高,而增、刪(不是尾部)的效率低,鏈表相反,HashMap則是把這兩者結(jié)合起來(lái),看下HashMap的數(shù)據(jù)結(jié)構(gòu)

從上面的結(jié)構(gòu)可以看出,通常情況下HashMap是以數(shù)組和鏈表的組合構(gòu)成(Java8中將鏈表長(zhǎng)度超過(guò)8的鏈表轉(zhuǎn)化成紅黑樹)。結(jié)合上面找手機(jī)的例子,我們簡(jiǎn)單分析下HashMap存取操作的心路歷程。put存一個(gè)鍵值對(duì)的時(shí)候(比如存上圖蓋倫),先根據(jù)鍵值"分類","分類"一頓操作后告訴我們,蓋倫應(yīng)該屬于14號(hào)坑,直接定位到14號(hào)坑。接下來(lái)有幾種情況:

  • 14號(hào)坑沒(méi)人,nice,直接存值;
  • 14號(hào)有人,也叫蓋倫,替換原來(lái)的攻擊值;
  • 14號(hào)有人,叫老王!插隊(duì)到老王前面去(單鏈表的頭插入方式,同一位置上新元素總會(huì)被放在鏈表的頭部位置)

get取的時(shí)候也需要傳鍵值,根據(jù)傳的鍵值來(lái)確定要找的是哪個(gè)"類別",比如找火男,"分類"一頓操作夠告訴我們火男屬于2號(hào)坑,于是我們直接定位到2號(hào)坑開始找,亞索不是…找到火男。

小結(jié)

HashMap是由數(shù)組和鏈表組合構(gòu)成的數(shù)據(jù)結(jié)構(gòu),Java8中鏈表長(zhǎng)度超過(guò)8時(shí)會(huì)把長(zhǎng)度超過(guò)8的鏈表轉(zhuǎn)化成紅黑樹;存取時(shí)都會(huì)根據(jù)鍵值計(jì)算出"類別"(hashCode),再根據(jù)"類別"定位到數(shù)組中的位置并執(zhí)行操作。

HashCode是什么

還是舉個(gè)栗子:一個(gè)工廠有500號(hào)人,下圖用兩種方案來(lái)存儲(chǔ)廠里員工的信件。

左右各有27個(gè)信箱,左邊保安大哥存信的時(shí)候不做處理,想放哪個(gè)信箱就放哪個(gè)信箱,當(dāng)員工去找信的時(shí)候,只好挨個(gè)信箱找,再挨個(gè)比對(duì)信箱里信封的名字,萬(wàn)一哥們臉黑,要找的放在最后一個(gè)信箱的最底下,悲劇…所以這種情況的時(shí)間復(fù)雜度為O(N);右邊采用HashCode的方式將27個(gè)信箱分類,分類的規(guī)則是名字首字母(第一個(gè)箱子放不寫名字的哥們),保安大哥將符合對(duì)應(yīng)姓名的信件放在對(duì)應(yīng)的信箱里,這樣員工就不用挨個(gè)找了,只需要比對(duì)一個(gè)信箱里的信件即可,大大提高了效率,這種情況的時(shí)間復(fù)雜度趨于一個(gè)常數(shù)O(1)。

例子中右圖其實(shí)就是hashCode的一個(gè)實(shí)現(xiàn),每個(gè)員工都有自己的hashCode,比如李四的hashCode是L,王五的hashCode是W(這取決于你的hash算法怎么寫),然后我們根據(jù)確定的hashCode值把信箱分類,hashCode匹配則存在對(duì)應(yīng)信箱。在Java的Object中可以調(diào)用hashCode()方法獲取對(duì)象hashCode,返回一個(gè)int值。那么會(huì)出現(xiàn)兩個(gè)對(duì)象的hashCode一樣嗎?答案是會(huì)的,就像上上個(gè)例子中蓋倫和老王的hashCode就一樣,這種情況網(wǎng)上有人稱之為"hash碰撞",出現(xiàn)這種所謂"碰撞"的處理上面已經(jīng)介紹了解決思路,具體源碼后續(xù)介紹。

小結(jié)

hashCode是一個(gè)對(duì)象的標(biāo)識(shí),Java中對(duì)象的hashCode是一個(gè)int類型值。通過(guò)hashCode來(lái)指定數(shù)組的索引可以快速定位到要找的對(duì)象在數(shù)組中的位置,之后再遍歷鏈表找到對(duì)應(yīng)值,理想情況下時(shí)間復(fù)雜度為O(1),并且不同對(duì)象可以擁有相同的hashCode。

HashMap的時(shí)間復(fù)雜度

通過(guò)上面信箱找信的例子來(lái)討論下HashMap的時(shí)間復(fù)雜度,在使用hashCode之后可以直接定位到一個(gè)箱子,時(shí)間的耗費(fèi)主要是在遍歷鏈表上,理想的情況下(hash算法寫得很完美),鏈表只有一個(gè)節(jié)點(diǎn),就是我們要的

那么此時(shí)的時(shí)間復(fù)雜度為O(1),那不理想的情況下(hash算法寫得很糟糕),比如上面信箱的例子,假設(shè)hash算法計(jì)算每個(gè)員工都返回同樣的hashCode

所有的信都放在一個(gè)箱子里,此時(shí)要找信就要依次遍歷C信箱里的信,時(shí)間復(fù)雜度不再是O(1),而是O(N),因此HashMap的時(shí)間復(fù)雜度取決于算法的實(shí)現(xiàn)上,當(dāng)然HashMap內(nèi)部的機(jī)制并不像信箱這么簡(jiǎn)單,在HashMap內(nèi)部會(huì)涉及到擴(kuò)容、Java8中會(huì)將長(zhǎng)度超過(guò)8的鏈表轉(zhuǎn)化成紅黑樹,這些都在后續(xù)介紹。

小結(jié)

HashMap的時(shí)間復(fù)雜度取決于hash算法,優(yōu)秀的hash算法可以讓時(shí)間復(fù)雜度趨于常數(shù)O(1),糟糕的hash算法可以讓時(shí)間復(fù)雜度趨于O(N)。

負(fù)載因子是什么

我們知道HashMap中數(shù)組長(zhǎng)度是16(什么?你說(shuō)不知道,看下源碼你就知道),假設(shè)我們用的是最優(yōu)秀的hash算法,即保證我每次往HashMap里存鍵值對(duì)的時(shí)候,都不會(huì)重復(fù),當(dāng)hashmap里有16個(gè)鍵值對(duì)的時(shí)候,要找到指定的某一個(gè),只需要1次;

之后繼續(xù)往里面存值,必然會(huì)發(fā)生所謂的"hash碰撞"形成鏈表,當(dāng)hashmap里有32個(gè)鍵值對(duì)時(shí),找到指定的某一個(gè)最壞情況要2次;當(dāng)hashmap里有128個(gè)鍵值對(duì)時(shí),找到指定的某一個(gè)最壞情況要8次

隨著hashmap里的鍵值對(duì)越來(lái)越多,在數(shù)組數(shù)量不變的情況下,查找的效率會(huì)越來(lái)越低。那怎么解決這個(gè)問(wèn)題呢?只要增加數(shù)組的數(shù)量就行了,鍵值對(duì)超過(guò)16,相應(yīng)的就要把數(shù)組的數(shù)量增加(HashMap內(nèi)部是原來(lái)的數(shù)組長(zhǎng)度乘以2),這就是網(wǎng)上所謂的擴(kuò)容,就算你有128個(gè)鍵值對(duì),我們準(zhǔn)備了128個(gè)坑,還是能保證"一個(gè)蘿卜一個(gè)坑"。

其實(shí)擴(kuò)容并沒(méi)有那么風(fēng)光,就像ArrayList一樣,擴(kuò)容是件很麻煩的事情,要?jiǎng)?chuàng)建一個(gè)新的數(shù)組,然后把原來(lái)數(shù)組里的鍵值對(duì)"放"到新的數(shù)組里,這里的"放"不像ArrayList那樣用原來(lái)的index,而是根據(jù)新表的長(zhǎng)度重新計(jì)算hashCode,來(lái)保證在新表的位置,老麻煩了,所以同一個(gè)鍵值對(duì)在舊數(shù)組里的索引和新數(shù)組中的索引通常是不一致的(火男:"我以前是3號(hào),怎么現(xiàn)在成了127號(hào),給我個(gè)完美的解釋!"新表:"大清亡了,現(xiàn)在你得聽我的")。另外,我們也可以看出這是典型的以空間換時(shí)間的操作。

說(shuō)了這么多,那負(fù)載因子是個(gè)什么東西?負(fù)載因子其實(shí)就是規(guī)定什么時(shí)候擴(kuò)容。上面我們說(shuō)默認(rèn)hashmap數(shù)組大小為16,存的鍵值對(duì)數(shù)量超過(guò)16則進(jìn)行擴(kuò)容,好像沒(méi)什么毛病。然而HashMap中并不是等數(shù)組滿了(達(dá)到16)才擴(kuò)容,它會(huì)存在一個(gè)閥值(threshold),只要hashmap里的鍵值對(duì)大于等于這個(gè)閥值,那么就要進(jìn)行擴(kuò)容。閥值的計(jì)算公式:

閥值 = 當(dāng)前數(shù)組長(zhǎng)度?負(fù)載因子

hashmap中默認(rèn)負(fù)載因子為0.75,默認(rèn)情況下第一次擴(kuò)容判斷閥值是16 ? 0.75 = 12;所以第一次存鍵值對(duì)的時(shí)候,在存到第13個(gè)鍵值對(duì)時(shí)就需要擴(kuò)容了;或者另外一種理解思路:假設(shè)當(dāng)前存到第12個(gè)鍵值對(duì):12 / 16 = 0.75,13 / 16 = 0.8125(大于0.75需要擴(kuò)容) 。肯定會(huì)有人有疑問(wèn),我要這鐵棒有何用?不,我要這負(fù)載因子有何用?直接規(guī)定超過(guò)數(shù)組長(zhǎng)度再擴(kuò)容不就行了,還省得每次擴(kuò)容之后還要重新計(jì)算新的閥值,Google說(shuō)取0.75是一個(gè)比較好的權(quán)衡,當(dāng)然我們可以自己修改,HashMap初識(shí)化時(shí)可以指定數(shù)組大小和負(fù)載因子,你完全可以改成1。

public HashMap(int initialCapacity, float loadFactor) 復(fù)制代碼

我的理解是這負(fù)載因子就像人的飯量,有的人吃要7分飽,有的人要10分飽,穩(wěn)妥起見默認(rèn)讓我們7.5分飽。

小結(jié)

在數(shù)組大小不變的情況下,存放鍵值對(duì)越多,查找的時(shí)間效率會(huì)降低,擴(kuò)容可以解決該問(wèn)題,而負(fù)載因子決定了什么時(shí)候擴(kuò)容,負(fù)載因子是已存鍵值對(duì)的數(shù)量和總的數(shù)組長(zhǎng)度的比值。默認(rèn)情況下負(fù)載因子為0.75,我們可在初始化HashMap的時(shí)候自己修改。

hash與Rehash

hash和rehash的概念其實(shí)上面已經(jīng)分析過(guò)了,每次擴(kuò)容后,轉(zhuǎn)移舊表鍵值對(duì)到新表之前都要重新rehash,計(jì)算鍵值對(duì)在新表的索引。如下圖火男這個(gè)鍵值對(duì)被存進(jìn)hashmap到后面擴(kuò)容,會(huì)經(jīng)過(guò)hash和rehash的過(guò)程

第一次hash可以理解成'"分類"',方便后續(xù)取、改等操作可以快速定位到具體的"坑"。那么為什么要進(jìn)行rehash,按照之前元素在數(shù)組中的索引直接賦值,例如火男之前3號(hào)坑,現(xiàn)在跑到30號(hào)坑。

個(gè)人理解是,在未擴(kuò)容前,可以看到如13號(hào)鏈的長(zhǎng)度是3,為了保證我們每次查找的時(shí)間復(fù)雜度O趨于O(1),理想的情況是"一個(gè)蘿卜一個(gè)坑",那么現(xiàn)在"坑"多了,原來(lái)"3個(gè)蘿卜一個(gè)坑"的情況現(xiàn)在就能有效的避免了。

源碼分析

Java7源碼分析

先看下Java7里的HashMap實(shí)現(xiàn),有了上面的分析,現(xiàn)在在源碼中找具體的實(shí)現(xiàn)。

//HashMap里的數(shù)組 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; //Entry對(duì)象,存key、value、hash值以及下一個(gè)節(jié)點(diǎn) static class Entry<K,V> implements Map.Entry<K,V> {final K key;V value;Entry<K,V> next;int hash; } //默認(rèn)數(shù)組大小,二進(jìn)制1左移4位為16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //負(fù)載因子默認(rèn)值 static final float DEFAULT_LOAD_FACTOR = 0.75f; //當(dāng)前存的鍵值對(duì)數(shù)量 transient int size; //閥值 = 數(shù)組大小 * 負(fù)載因子 int threshold; //負(fù)載因子變量 final float loadFactor;//默認(rèn)new HashMap數(shù)組大小16,負(fù)載因子0.75 public HashMap() {this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); }public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR); } //可以指定數(shù)組大小和負(fù)載因子 public HashMap(int initialCapacity, float loadFactor) {//省略一些邏輯判斷this.loadFactor = loadFactor;threshold = initialCapacity;//空方法init(); } 復(fù)制代碼

以上就是HashMap的一些先決條件,接著看平時(shí)put操作的代碼實(shí)現(xiàn),put的時(shí)候會(huì)遇到3種情況上面已分析過(guò),看下Java7代碼:

public V put(K key, V value) {//數(shù)組為空時(shí)創(chuàng)建數(shù)組if (table == EMPTY_TABLE) {inflateTable(threshold);}//key為空單獨(dú)對(duì)待if (key == null)return putForNullKey(value);//①根據(jù)key計(jì)算hash值int hash = hash(key);//②根據(jù)hash值和當(dāng)前數(shù)組的長(zhǎng)度計(jì)算在數(shù)組中的索引int i = indexFor(hash, table.length);//遍歷整條鏈表for (Entry<K,V> e = table[i]; e != null; e = e.next) {Object k;//③情況1.hash值和key值都相同的情況,替換之前的值if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {V oldValue = e.value;e.value = value;e.recordAccess(this);//返回被替換的值return oldValue;}}modCount++;//③情況2.坑位沒(méi)人,直接存值或發(fā)生hash碰撞都走這addEntry(hash, key, value, i);return null;} 復(fù)制代碼

先看上面key為空的情況(上面畫圖的時(shí)候總要在第一格留個(gè)空key的鍵值對(duì)),執(zhí)行 putForNullKey() 方法單獨(dú)處理,會(huì)把該鍵值對(duì)放在index0,所以HashMap中是允許key為空的情況。再看下主流程:

步驟①.根據(jù)鍵值算出hash值 — > hash(key)

步驟②.根據(jù)hash值和當(dāng)前數(shù)組的長(zhǎng)度計(jì)算在數(shù)組中的索引 — > indexFor(hash, table.length)

static int indexFor(int h, int length) {//hash值和數(shù)組長(zhǎng)度-1按位與操作,聽著費(fèi)勁?其實(shí)相當(dāng)于h%length;取余數(shù)(取模運(yùn)算)//如:h = 17,length = 16;那么算出就是1//&運(yùn)算的效率比%要高return h & (length-1);} 復(fù)制代碼

步驟③情況1.hash值和key值都相同,替換原來(lái)的值,并將被替換的值返回。

步驟③情況2.坑位沒(méi)人或發(fā)生hash碰撞 — > addEntry(hash, key, value, i)

void addEntry(int hash, K key, V value, int bucketIndex) {//當(dāng)前hashmap中的鍵值對(duì)數(shù)量超過(guò)閥值if ((size >= threshold) && (null != table[bucketIndex])) {//擴(kuò)容為原來(lái)的2倍resize(2 * table.length);hash = (null != key) ? hash(key) : 0;//計(jì)算在新表中的索引bucketIndex = indexFor(hash, table.length);}//創(chuàng)建節(jié)點(diǎn)createEntry(hash, key, value, bucketIndex);} 復(fù)制代碼

如果put的時(shí)候超過(guò)閥值,會(huì)調(diào)用 resize() 方法將數(shù)組大小擴(kuò)大為原來(lái)的2倍,并且根據(jù)新表的長(zhǎng)度計(jì)算在新表中的索引(如之前17%16 =1,現(xiàn)在17%32=17),看下resize方法

void resize(int newCapacity) { //傳入新的容量//獲取舊數(shù)組的引用Entry[] oldTable = table;int oldCapacity = oldTable.length;//極端情況,當(dāng)前鍵值對(duì)數(shù)量已經(jīng)達(dá)到最大if (oldCapacity == MAXIMUM_CAPACITY) {//修改閥值為最大直接返回threshold = Integer.MAX_VALUE;return;}//步驟①根據(jù)容量創(chuàng)建新的數(shù)組Entry[] newTable = new Entry[newCapacity];//步驟②將鍵值對(duì)轉(zhuǎn)移到新的數(shù)組中transfer(newTable, initHashSeedAsNeeded(newCapacity));//步驟③將新數(shù)組的引用賦給tabletable = newTable;//步驟④修改閥值threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);}復(fù)制代碼

上面的重點(diǎn)是步驟②,看下它具體的轉(zhuǎn)移操作

void transfer(Entry[] newTable, boolean rehash) {//獲取新數(shù)組的長(zhǎng)度int newCapacity = newTable.length;//遍歷舊數(shù)組中的鍵值對(duì)for (Entry<K,V> e : table) {while(null != e) {Entry<K,V> next = e.next;if (rehash) {e.hash = null == e.key ? 0 : hash(e.key);}//計(jì)算在新表中的索引,并到新數(shù)組中int i = indexFor(e.hash, newCapacity);e.next = newTable[i];newTable[i] = e;e = next;}}} 復(fù)制代碼

這段for循環(huán)的遍歷會(huì)使得轉(zhuǎn)移前后鍵值對(duì)的順序顛倒(Java7和Java8的區(qū)別),畫個(gè)圖就清楚了,假設(shè)石頭的key值為5,蓋倫的key值為37,這樣擴(kuò)容前后兩者還是在5號(hào)坑。第一次:

第二次

最后再看下創(chuàng)建節(jié)點(diǎn)的方法

void createEntry(int hash, K key, V value, int bucketIndex) {Entry<K,V> e = table[bucketIndex];table[bucketIndex] = new Entry<>(hash, key, value, e);size++;} 復(fù)制代碼

創(chuàng)建節(jié)點(diǎn)時(shí),如果找到的這個(gè)坑里面沒(méi)有存值,那么直接把值存進(jìn)去就行了,然后size++;如果是碰撞的情況,

前面說(shuō)的以單鏈表頭插入的方式就是這樣(蓋倫:”老王已被我一腳踢開!“),總結(jié)一下Java7 put流程圖

相比put,get操作就沒(méi)這么多套路,只需要根據(jù)key值計(jì)算hash值,和數(shù)組長(zhǎng)度取模,然后就可以找到在數(shù)組中的位置(key為空同樣單獨(dú)操作),接著就是遍歷鏈表,源碼很少就不分析了。

Java8源碼分析

基本思路是一樣的

//定義長(zhǎng)度超過(guò)8的鏈表轉(zhuǎn)化成紅黑樹 static final int TREEIFY_THRESHOLD = 8; //換了個(gè)馬甲還是認(rèn)識(shí)你!!! static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Node<K,V> next; } 復(fù)制代碼

看下Java8 put的源碼

public V put(K key, V value) {//根據(jù)key計(jì)算hashreturn 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.數(shù)組為空或數(shù)組長(zhǎng)度為0,則擴(kuò)容(咦,看到不一樣咯)if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//步驟2.根據(jù)hash值和數(shù)組長(zhǎng)度計(jì)算在數(shù)組中的位置//如果"坑"里沒(méi)人,直接創(chuàng)建Node并存值if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;//步驟3."坑"里有人,且hash值和key值都相等,先獲取引用,后面會(huì)用來(lái)替換值if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;//步驟4.該鏈?zhǔn)羌t黑樹else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//步驟5.該鏈?zhǔn)擎湵?span id="ozvdkddzhkzd" class="hljs-keyword">else {for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {//步驟5.1注意這個(gè)地方跟Java7不一樣,是插在鏈表尾部!!!p.next = newNode(hash, key, value, null);//鏈表長(zhǎng)度超過(guò)8,轉(zhuǎn)化成紅黑樹if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}//步驟5.2鏈表中已存在且hash值和key值都相等,先獲取引用,后面用來(lái)替換值if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)//統(tǒng)一替換原來(lái)的值e.value = value;afterNodeAccess(e);//返回原來(lái)的值return oldValue;}}++modCount;//步驟6.鍵值對(duì)數(shù)量超過(guò)閥值,擴(kuò)容if (++size > threshold)resize();afterNodeInsertion(evict);return null;} 復(fù)制代碼

通過(guò)上面注釋分析,對(duì)比和Java7的區(qū)別,Java8一視同仁,管你key為不為空的統(tǒng)一處理,多了一步鏈表長(zhǎng)度的判斷以及轉(zhuǎn)紅黑樹的操作,并且比較重要的一點(diǎn),新增Node是插在尾部而不是頭部!!!。當(dāng)然上面的主角還是擴(kuò)容resize操作

final Node<K,V>[] resize() {//舊數(shù)組的引用Node<K,V>[] oldTab = table;//舊數(shù)組長(zhǎng)度int oldCap = (oldTab == null) ? 0 : oldTab.length;//舊數(shù)組閥值int oldThr = threshold;//新數(shù)組長(zhǎng)度、新閥值int newCap, newThr = 0;if (oldCap > 0) {//極端情況,舊數(shù)組爆滿了if (oldCap >= MAXIMUM_CAPACITY) {//閥值改成最大,放棄治療直接返回舊數(shù)組threshold = Integer.MAX_VALUE;return oldTab;}//擴(kuò)容咯,這里采用左移運(yùn)算左移1位,也就是舊數(shù)組*2else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)//同樣新閥值也是舊閥值*2newThr = oldThr << 1; // double threshold}else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;//初始化在這里else { // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}//更新閥值threshold = newThr;@SuppressWarnings({"rawtypes","unchecked"})//創(chuàng)建新數(shù)組Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {//遍歷舊數(shù)組,把原來(lái)的引用取消,方便垃圾回收oldTab[j] = null;//這個(gè)鏈只有一個(gè)節(jié)點(diǎn),根據(jù)新數(shù)組長(zhǎng)度計(jì)算在新表中的位置if (e.next == null)newTab[e.hash & (newCap - 1)] = e;//紅黑樹的處理else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);//鏈表長(zhǎng)度大于1,小于8的情況,下面高能,單獨(dú)拿出來(lái)分析else { // preserve orderNode<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab; } 復(fù)制代碼

可以看到,Java8把初始化數(shù)組和擴(kuò)容全寫在resize方法里了,但是思路還是一樣的,擴(kuò)容后要轉(zhuǎn)移,轉(zhuǎn)移要重新計(jì)算在新表中的位置,上面代碼最后一塊高能可能不太好理解,剛開始看的我一臉懵逼,看了一張美團(tuán)博客的分析圖才豁然開朗,在分析前先捋清楚思路

下面我們講解下JDK1.8做了哪些優(yōu)化。經(jīng)過(guò)觀測(cè)可以發(fā)現(xiàn),我們使用的是2次冪的擴(kuò)展(指長(zhǎng)度擴(kuò)為原來(lái)2倍),所以,元素的位置要么是在原位置,要么是在原位置再移動(dòng)2次冪的位置。看下圖可以明白這句話的意思,n為table的長(zhǎng)度,圖(a)表示擴(kuò)容前的key1(5)和key2(21)兩種key確定索引位置的示例,圖(b)表示擴(kuò)容后key1和key2兩種key確定索引位置的示例,其中hash1是key1對(duì)應(yīng)的哈希與高位運(yùn)算結(jié)果。

圖a中key1(5)和key(21)計(jì)算出來(lái)的都是5,元素在重新計(jì)算hash之后,因?yàn)閚變?yōu)?倍,那么n-1的mask范圍在高位多1bit(紅色),因此新的index就會(huì)發(fā)生這樣的變化:

圖b中計(jì)算后key1(5)的位置還是5,而key2(21)已經(jīng)變成了21,因此,我們?cè)跀U(kuò)充HashMap的時(shí)候,不需要像JDK1.7的實(shí)現(xiàn)那樣重新計(jì)算hash,只需要看看原來(lái)的hash值新增的那個(gè)bit是1還是0就好了,是0的話索引沒(méi)變,是1的話索引變成“原索引+oldCap”。

有了上面的分析再回來(lái)看下源碼

else { // preserve order//定義兩條鏈//原來(lái)的hash值新增的bit為0的鏈,頭部和尾部Node<K,V> loHead = null, loTail = null;//原來(lái)的hash值新增的bit為1的鏈,頭部和尾部Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;//循環(huán)遍歷出鏈條鏈do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);//擴(kuò)容前后位置不變的鏈if (loTail != null) {loTail.next = null;newTab[j] = loHead;}//擴(kuò)容后位置加上原數(shù)組長(zhǎng)度的鏈if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;} } 復(fù)制代碼

為了更清晰明了,還是舉個(gè)栗子,下面的表定義了鍵和它們的hash值(數(shù)組長(zhǎng)度為16時(shí),它們都在5號(hào)坑)

KeyHash
石頭5
蓋倫5
蒙多5
妖姬21
狐貍21
日女21

假設(shè)一個(gè)hash算法剛好算出來(lái)的的存儲(chǔ)是這樣的,在存第13個(gè)元素時(shí)要擴(kuò)容

那么流程應(yīng)該是這樣的(只關(guān)注5號(hào)坑鍵值對(duì)的情況),第一次:

第二次:

省略中間幾次,第六次

兩條鏈找出來(lái)后,最后轉(zhuǎn)移一波,大功告成。

//擴(kuò)容前后位置不變的鏈if (loTail != null) {loTail.next = null;newTab[j] = loHead;}//擴(kuò)容后位置加上原數(shù)組長(zhǎng)度的鏈if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;} 復(fù)制代碼

總結(jié)下Java8 put流程圖

對(duì)比

1.發(fā)生hash沖突時(shí),Java7會(huì)在鏈表頭部插入,Java8會(huì)在鏈表尾部插入

2.擴(kuò)容后轉(zhuǎn)移數(shù)據(jù),Java7轉(zhuǎn)移前后鏈表順序會(huì)倒置,Java8還是保持原來(lái)的順序

3.關(guān)于性能對(duì)比可以參考美團(tuán)技術(shù)博客,引入紅黑樹的Java8大程度得優(yōu)化了HashMap的性能

感謝

講的很詳細(xì)的外國(guó)小哥

美團(tuán)技術(shù)博客

總結(jié)

以上是生活随笔為你收集整理的图解HashMap(一)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。