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

歡迎訪問 生活随笔!

生活随笔

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

编程问答

hashmap转红黑树的阈值为8_面试必考的 HashMap,这篇总结到位了

發(fā)布時間:2025/3/11 编程问答 39 豆豆
生活随笔 收集整理的這篇文章主要介紹了 hashmap转红黑树的阈值为8_面试必考的 HashMap,这篇总结到位了 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
點擊藍(lán)色“JavaKeeper”關(guān)注我喲加個“星標(biāo)”,一起成長,做牛逼閃閃的技術(shù)人

1 概述

HashMap是基于哈希表實現(xiàn)的,每一個元素是一個key-value對,其內(nèi)部通過單鏈表解決沖突問題,容量不足(超過了閥值)時,同樣會自動增長.

HashMap是非線程安全的,只適用于單線程環(huán)境,多線程環(huán)境可以采用并發(fā)包下的concurrentHashMap

HashMap 實現(xiàn)了Serializable接口,因此它支持序列化,實現(xiàn)了Cloneable接口,能被克隆

HashMap是基于哈希表的Map接口的非同步實現(xiàn).此實現(xiàn)提供所有可選的映射操作,并允許使用null值和null鍵.此類不保證映射的順序,特別是它不保證該順序恒久不變.

Java8中又對此類底層實現(xiàn)進行了優(yōu)化,比如引入了紅黑樹的結(jié)構(gòu)以解決哈希碰撞

2 HashMap的數(shù)據(jù)結(jié)構(gòu)

在Java中,最基本的結(jié)構(gòu)就是兩種,一個是數(shù)組,另外一個是模擬指針(引用),所有的數(shù)據(jù)結(jié)構(gòu)都可以用這兩個基本結(jié)構(gòu)來構(gòu)造,HashMap也不例外. HashMap實際上是一個"鏈表散列"的數(shù)據(jù)結(jié)構(gòu),即數(shù)組和鏈表的結(jié)合體.

HashMap的主結(jié)構(gòu)類似于一個數(shù)組,添加值時通過key確定儲存位置.
每個位置是一個Entry的數(shù)據(jù)結(jié)構(gòu),該結(jié)構(gòu)可組成鏈表.
當(dāng)發(fā)生沖突時,相同hash值的鍵值對會組成鏈表.
這種數(shù)組+鏈表的組合形式大部分情況下都能有不錯的性能效果,Java6、7就是這樣設(shè)計的. 然而,在極端情況下,一組(比如經(jīng)過精心設(shè)計的)鍵值對都發(fā)生了沖突,這時的哈希結(jié)構(gòu)就會退化成一個鏈表,使HashMap性能急劇下降.

所以在Java8中,HashMap的結(jié)構(gòu)實現(xiàn)變?yōu)閿?shù)組+鏈表+紅黑樹

可以看出,HashMap底層就是一個數(shù)組結(jié)構(gòu)
數(shù)組中的每一項又是一個鏈表
當(dāng)新建一個HashMap時,就會初始化一個數(shù)組.

3 三大集合與迭代子

HashMap使用三大集合和三種迭代子來輪詢其Key、Value和Entry對象

public?class?HashMapExam?{
????public?static?void?main(String[]?args)?{
????????Map?map?=?new?HashMap(16);
????????for?(int?i?=?0;?i?????????????map.put(i,?new?String(new?char[]{(char)?('A'+?i)}));
????????}
????????System.out.println("======keySet=======");
????????Set?set?=?map.keySet();
????????Iterator?iterator?=?set.iterator();
????????while?(iterator.hasNext())?{
????????????System.out.println(iterator.next());
????????}
????????System.out.println("======values=======");
????????Collection?values?=?map.values();
????????Iterator?stringIterator=values.iterator();
????????while?(stringIterator.hasNext())?{
????????????System.out.println(stringIterator.next());
????????}
????????System.out.println("======entrySet=======");
????????for?(Map.Entry?entry?:?map.entrySet())?{
????????????System.out.println(entry);
????????}
????}
}

4 源碼分析

//默認(rèn)的初始容量16,且實際容量是2的整數(shù)冪?
????static?final?int?DEFAULT_INITIAL_CAPACITY?=?1?<????//最大容量(傳入容量過大將被這個值替換)
????static?final?int?MAXIMUM_CAPACITY?=?1?<????//?默認(rèn)加載因子為0.75(當(dāng)表達到3/4滿時,才會再散列),這個因子在時間和空間代價之間達到了平衡.更高的因子可以降低表所需的空間,但是會增加查找代價,而查找是最頻繁操作
????static?final?float?DEFAULT_LOAD_FACTOR?=?0.75f;
????//桶的樹化閾值:即?鏈表轉(zhuǎn)成紅黑樹的閾值,在存儲數(shù)據(jù)時,當(dāng)鏈表長度?>= 8時,則將鏈表轉(zhuǎn)換成紅黑樹
????static?final?int?TREEIFY_THRESHOLD?=?8;
???//?桶的鏈表還原閾值:即?紅黑樹轉(zhuǎn)為鏈表的閾值,當(dāng)在擴容(resize())時(HashMap的數(shù)據(jù)存儲位置會重新計算),在重新計算存儲位置后,當(dāng)原有的紅黑樹內(nèi)數(shù)量?<= 6時,則將?紅黑樹轉(zhuǎn)換成鏈表
????static?final?int?UNTREEIFY_THRESHOLD?=?6;
???//最小樹形化容量閾值:即?當(dāng)哈希表中的容量?>?該值時,才允許樹形化鏈表?(即?將鏈表?轉(zhuǎn)換成紅黑樹)

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

還有選擇6和8,中間有個差值7可以有效防止鏈表和樹頻繁轉(zhuǎn)換
假設(shè)一下,如果設(shè)計成鏈表個數(shù)超過8則鏈表轉(zhuǎn)換成樹結(jié)構(gòu),鏈表個數(shù)小于8則樹結(jié)構(gòu)轉(zhuǎn)換成鏈表,如果一個HashMap不停的插入、刪除元素,鏈表個數(shù)在8左右徘徊,就會頻繁的發(fā)生樹轉(zhuǎn)鏈表、鏈表轉(zhuǎn)樹,效率會很低。

//?為了避免擴容/樹形化選擇的沖突,這個值不能小于?4?*?TREEIFY_THRESHOLD
????//?小于該值時使用的是擴容哦!!!
????static?final?int?MIN_TREEIFY_CAPACITY?=?64;
????//?存儲數(shù)據(jù)的Node數(shù)組,長度是2的冪.????
????//?HashMap采用鏈表法解決沖突,每一個Node本質(zhì)上是一個單向鏈表?
????//HashMap底層存儲的數(shù)據(jù)結(jié)構(gòu),是一個Node數(shù)組.上面得知Node類為元素維護了一個單向鏈表.至此,HashMap存儲的數(shù)據(jù)結(jié)構(gòu)也就很清晰了:維護了一個數(shù)組,每個數(shù)組又維護了一個單向鏈表.之所以這么設(shè)計,考慮到遇到哈希沖突的時候,同index的value值就用單向鏈表來維護
????//與?JDK?1.7?的對比(Entry類),僅僅只是換了名字
????transient?Node[]?table;
????//?HashMap的底層數(shù)組中已用槽的數(shù)量?
????transient?int?size;
????//?HashMap的閾值,用于判斷是否需要調(diào)整HashMap的容量(threshold?=?容量*加載因子)?
????int?threshold;
????//?負(fù)載因子實際大小
????final?float?loadFactor;
????//?HashMap被改變的次數(shù)?
????transient?int?modCount;
????//?指定“容量大小”和“加載因子”的構(gòu)造函數(shù),是最基礎(chǔ)的構(gòu)造函數(shù)
????public?HashMap(int?initialCapacity,?float?loadFactor)?{
????????if?(initialCapacity?????????????throw?new?IllegalArgumentException("Illegal?initial?capacity:?"?+
???????????????????????????????????????????????initialCapacity);
????????//?HashMap的最大容量只能是MAXIMUM_CAPACITY???????????????????????????????????????
????????if?(initialCapacity?>?MAXIMUM_CAPACITY)
????????????initialCapacity?=?MAXIMUM_CAPACITY;
????????//負(fù)載因子須大于0
????????if?(loadFactor?<=?0?||?Float.isNaN(loadFactor))
????????????throw?new?IllegalArgumentException("Illegal?load?factor:?"?+
???????????????????????????????????????????????loadFactor);
????????//?設(shè)置"負(fù)載因子"????????????????????????????????????????
????????this.loadFactor?=?loadFactor;
????????//?設(shè)置"HashMap閾值",當(dāng)HashMap中存儲數(shù)據(jù)的數(shù)量達到threshold時,就需將HashMap的容量加倍????
????????this.threshold?=?tableSizeFor(initialCapacity);
????}

上面的tableSizeFor有何用?
tableSizeFor方法保證函數(shù)返回值是大于等于給定參數(shù)initialCapacity最小的2的冪次方的數(shù)值

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?=?MAXIMUM_CAPACITY)???MAXIMUM_CAPACITY?:?n?+?1;
??}

a |= b 等同于 a = a|b

逐行分析

  • int n = cap - 1
    給定的cap 減 1,為了避免參數(shù)cap本來就是2的冪次方,這樣一來,經(jīng)過后續(xù)操作,cap將會變成2 * cap,是不符合我們預(yù)期的

  • n |= n >>> 1
    n >>> 1 : n無符號右移1位,即n二進制最高位的1右移一位
    n | (n >>> 1) 導(dǎo)致 n二進制的高2位值為1
    目前n的高1~2位均為1

  • n |= n >>> 2
    n繼續(xù)無符號右移2位
    n | (n >>> 2) 導(dǎo)致n二進制表示的高34位經(jīng)過運算值均為1
    目前n的高14位均為1

  • n |= n >>> 4
    n繼續(xù)無符號右移4位
    n | (n >>> 4) 導(dǎo)致n二進制表示的高58位經(jīng)過運算值均為1
    目前n的高18位均為1

  • n |= n >>> 8
    n繼續(xù)無符號右移8位
    n | (n >>> 8) 導(dǎo)致n二進制表示的高916位經(jīng)過運算值均為1
    目前n的高116位均為1

可以看出,無論給定cap(cap < MAXIMUM_CAPACITY )的值是多少,經(jīng)過以上運算,其值的二進制所有位都會是1.再將其加1,這時候這個值一定是2的冪次方.
當(dāng)然如果經(jīng)過運算值大于MAXIMUM_CAPACITY,直接選用MAXIMUM_CAPACITY.

至此tableSizeFor如何保證cap為2的冪次方已經(jīng)顯而易見了,那么問題來了

4.1 為什么cap要保持為2的冪次方?

主要與HashMap中的數(shù)據(jù)存儲有關(guān).

在Java8中,HashMap中key的Hash值由Hash(key)方法計得

HashMap中存儲數(shù)據(jù)table的index是由key的Hash值決定的.
在HashMap存儲數(shù)據(jù)時,我們期望數(shù)據(jù)能均勻分布,以防止哈希沖突.
自然而然我們就會想到去用%取余操作來實現(xiàn)我們這一構(gòu)想

取余(%)操作 : 如果除數(shù)是2的冪次則等價于與其除數(shù)減一的與(&)操作.

這也就解釋了為什么一定要求cap要為2的冪次方.再來看看table的index的計算規(guī)則:

等價于:index = e.hash % newCap

采用二進制位操作&,相對于%,能夠提高運算效率,這就是cap的值被要求為2冪次的原因

4.2 Node類

static?class?Node?implements?Map.Entry?{
????????final?int?hash;
????????final?K?key;
????????V?value;
????????Node?next;
????????Node(int?hash,?K?key,?V?value,?Node?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()?{
????????????return?Objects.hashCode(key)?^?Objects.hashCode(value);
????????}
????????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;
????????}
????}

Node 類是HashMap中的靜態(tài)內(nèi)部類,實現(xiàn)Map.Entry接口.定義了key鍵、value值、next節(jié)點,也就是說元素之間構(gòu)成了單向鏈表.

4.3 TreeNode

static?final?class?TreeNode?extends?LinkedHashMap.Entry?{
????????TreeNode?parent;??//?red-black?tree?links
????????TreeNode?left;
????????TreeNode?right;
????????TreeNode?prev;????//?needed?to?unlink?next?upon?deletion
????????boolean?red;
????????TreeNode(int?hash,?K?key,?V?val,?Node?next)?{}
????????//?返回當(dāng)前節(jié)點的根節(jié)點??
????????final?TreeNode?root()?{??
??????????for?(TreeNode?r?=?this,?p;;)?{??
????????????if?((p?=?r.parent)?==?null)??
????????????????return?r;??
????????????r?=?p;??
????????}??
????}?
?}

紅黑樹結(jié)構(gòu)包含前、后、左、右節(jié)點,以及標(biāo)志是否為紅黑樹的字段
此結(jié)構(gòu)是Java8新加的

4.4 hash方法

Java 8中的散列值優(yōu)化函數(shù)

只做一次16位右位移異或
key.hashCode()函數(shù)調(diào)用的是key鍵值類型自帶的哈希函數(shù),返回int型散列值

理論上散列值是一個int型,如果直接拿散列值作為下標(biāo)訪問HashMap主數(shù)組的話,考慮到2進制32位帶符號的int范圍大概40億的映射空間。只要哈希函數(shù)映射得比較均勻松散,一般應(yīng)用是很難出現(xiàn)碰撞的。
但問題是一個40億長度的數(shù)組,內(nèi)存是放不下的.HashMap擴容之前的數(shù)組初始大小才16,所以這個散列值是不能直接拿來用的.
用之前還要先做對數(shù)組的長度取模運算,得到的余數(shù)才能用來訪問數(shù)組下標(biāo)
源碼中模運算就是把散列值和數(shù)組長度做一個"與"操作,

這也正好解釋了為什么HashMap的數(shù)組長度要取2的整次冪
因為這樣(數(shù)組長度-1)正好相當(dāng)于一個“低位掩碼”
“與”操作的結(jié)果就是散列值的高位全部歸零,只保留低位值,用來做數(shù)組下標(biāo)訪問

以初始長度16為例,16-1=15
2進制表示是00000000 00000000 00001111
和某散列值做“與”操作如下,結(jié)果就是截取了最低的四位值

但這時候問題就來了,這樣就算我的散列值分布再松散,要是只取最后幾位的話,碰撞也會很嚴(yán)重

這時候“擾動函數(shù)”的價值就體現(xiàn)出來了

右位移16位,正好是32位一半,自己的高半?yún)^(qū)和低半?yún)^(qū)做異或,就是為了混合原始hashCode的高位和低位,以此來加大低位的隨機性
而且混合后的低位摻雜了高位的部分特征,這樣高位的信息也被變相保留下來。

index的運算規(guī)則是e.hash & (newCap - 1)
newCap是2的冪,所以newCap - 1的高位全0

若e.hash值只用自身的hashcode,index只會和e.hash的低位做&操作.這樣一來,index的值就只有低位參與運算,高位毫無存在感,從而會帶來哈希沖突的風(fēng)險
所以在計算key的hashCode時,用其自身hashCode與其低16位做異或操作
這也就讓高位參與到index的計算中來了,即降低了哈希沖突的風(fēng)險又不會帶來太大的性能問題

4.5 Put方法

①.判斷鍵值對數(shù)組table[i]是否為空或為null,否則執(zhí)行resize()進行擴容

②.根據(jù)鍵值key計算hash值得到插入的數(shù)組索引i,如果table[i]==null,直接新建節(jié)點添加,轉(zhuǎn)向⑥,如果table[i]不為空,轉(zhuǎn)向③

③.判斷table[i]的首個元素是否和key一樣,如果相同直接覆蓋value,否則轉(zhuǎn)向④,這里的相同指的是hashCode以及equals

④.判斷table[i] 是否為treeNode,即table[i] 是否是紅黑樹,如果是紅黑樹,則直接在樹中插入鍵值對,否則轉(zhuǎn)向⑤

⑤.遍歷table[i],判斷鏈表長度是否大于8,大于8的話把鏈表轉(zhuǎn)換為紅黑樹,在紅黑樹中執(zhí)行插入操作,否則進行鏈表的插入操作;遍歷過程中若發(fā)現(xiàn)key已經(jīng)存在直接覆蓋value即可

⑥.插入成功后,判斷實際存在的鍵值對數(shù)量size是否超多了最大容量threshold,如果超過,執(zhí)行resize()擴容

public?V?put(K?key,?V?value)?{
????????//?對key的hashCode()做hash
????????return?putVal(hash(key),?key,?value,?false,?true);
????}
final?V?putVal(int?hash,?K?key,?V?value,?boolean?onlyIfAbsent,?boolean?evict)?{
????????Node[]?tab;?Node?p;?int?n,?i;
????????//?步驟①?tab為空則調(diào)用resize()初始化創(chuàng)建
????????if?((tab?=?table)?==?null?||?(n?=?tab.length)?==?0)?????????
????????????n?=?(tab?=?resize()).length;
????????//?步驟②?計算index,并對null做處理??
????????//tab[i?=?(n?-?1)?&?hash對應(yīng)下標(biāo)的第一個節(jié)點???
????????if?((p?=?tab[i?=?(n?-?1)?&?hash])?==?null)
????????????//?無哈希沖突的情況下,將value直接封裝為Node并賦值
????????????tab[i]?=?newNode(hash,?key,?value,?null);
????????else?{
????????????Node?e;?K?k;
????????????//?步驟③?節(jié)點的key相同,直接覆蓋節(jié)點
????????????if?(p.hash?==?hash?&&?((k?=?p.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
????????????????e?=?p;
????????????//?步驟④?判斷該鏈為紅黑樹????
????????????else?if?(p?instanceof?TreeNode)
?????????????????//?p是紅黑樹類型,則調(diào)用putTreeVal方式賦值
????????????????e?=?((TreeNode)p).putTreeVal(this,?tab,?hash,?key,?value);
????????????//?步驟⑤?p非紅黑樹類型,該鏈為鏈表????
????????????else?{
????????????????//?index?相同的情況下
????????????????for?(int?binCount?=?0;?;?++binCount)?{
????????????????????if?((e?=?p.next)?==?null)?{
????????????????????????//?如果p的next為空,將新的value值添加至鏈表后面
????????????????????????p.next?=?newNode(hash,?key,?value,?null);
????????????????????????if?(binCount?>=?TREEIFY_THRESHOLD?-?1)
????????????????????????????//?如果鏈表長度大于8,鏈表轉(zhuǎn)化為紅黑樹,執(zhí)行插入
????????????????????????????treeifyBin(tab,?hash);
????????????????????????break;
????????????????????}
????????????????????//?key相同則跳出循環(huán)
????????????????????if?(e.hash?==?hash?&&??((k?=?e.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
????????????????????????break;
????????????????????//就是移動指針方便繼續(xù)取?p.next
????????????????????p?=?e;
????????????????}
????????????}
????????????if?(e?!=?null)?{?//?existing?mapping?for?key
????????????????V?oldValue?=?e.value;
????????????????//根據(jù)規(guī)則選擇是否覆蓋value
????????????????if?(!onlyIfAbsent?||?oldValue?==?null)
????????????????????e.value?=?value;
????????????????afterNodeAccess(e);
????????????????return?oldValue;
????????????}
????????}
????????++modCount;
????????//?步驟⑥:超過最大容量,就擴容
????????if?(++size?>?threshold)
????????????//?size大于加載因子,擴容
????????????resize();
????????afterNodeInsertion(evict);
????????return?null;
????}

在構(gòu)造函數(shù)中最多也只是設(shè)置了initialCapacity、loadFactor的值,并沒有初始化table,table的初始化工作是在put方法中進行的.

4.6 resize


擴容(resize)就是重新計算容量,向HashMap對象里不停的添加元素,內(nèi)部的數(shù)組無法裝載更多的元素時,就需要擴大數(shù)組的長度.
當(dāng)然Java里的數(shù)組是無法自動擴容的,方法是使用一個新的數(shù)組代替已有的容量小的數(shù)組

/**
?????*?該函數(shù)有2種使用情況:1.初始化哈希表 2.當(dāng)前數(shù)組容量過小,需擴容
?????*/
final?Node[]?resize()?{
????????Node[]?oldTab?=?table;
????????int?oldCap?=?(oldTab?==?null)???0?:?oldTab.length;
????????int?oldThr?=?threshold;
????????int?newCap,?newThr?=?0;
????????//?針對情況2:若擴容前的數(shù)組容量超過最大值,則不再擴充
????????if?(oldCap?>?0)?{
????????????if?(oldCap?>=?MAXIMUM_CAPACITY)?{
????????????????threshold?=?Integer.MAX_VALUE;
????????????????return?oldTab;
????????????}
????????????//?針對情況2:若無超過最大值,就擴充為原來的2倍
????????????else?if?((newCap?=?oldCap?<?????????????????????oldCap?>=?DEFAULT_INITIAL_CAPACITY)
????????????????//newCap設(shè)置為oldCap的2倍并小于MAXIMUM_CAPACITY,且大于默認(rèn)值,?新的threshold增加為原來的2倍
????????????????newThr?=?oldThr?<????????}
????????//?針對情況1:初始化哈希表(采用指定 or 默認(rèn)值)
????????else?if?(oldThr?>?0)?//?initial?capacity?was?placed?in?threshold
????????????//?threshold>0,?將threshold設(shè)置為newCap,所以要用tableSizeFor方法保證threshold是2的冪次方
????????????newCap?=?oldThr;
????????else?{???????????????//?zero?initial?threshold?signifies?using?defaults
????????????//?默認(rèn)初始化
????????????newCap?=?DEFAULT_INITIAL_CAPACITY;
????????????newThr?=?(int)(DEFAULT_LOAD_FACTOR?*?DEFAULT_INITIAL_CAPACITY);
????????}
????????//?計算新的resize上限
????????if?(newThr?==?0)?{
????????????//?newThr為0,newThr?=?newCap?*?0.75
????????????float?ft?=?(float)newCap?*?loadFactor;
????????????newThr?=?(newCap?float)MAXIMUM_CAPACITY??
??????????????????????(int)ft?:?Integer.MAX_VALUE);
????????}
????????threshold?=?newThr;
????????@SuppressWarnings({"rawtypes","unchecked"})
????????????//?新生成一個table數(shù)組
????????????Node[]?newTab?=?(Node[])new?Node[newCap];
????????table?=?newTab;
????????if?(oldTab?!=?null)?{
????????????//?oldTab?復(fù)制到?newTab
????????????for?(int?j?=?0;?j?????????????????Node?e;
????????????????if?((e?=?oldTab[j])?!=?null)?{
????????????????????oldTab[j]?=?null;
????????????????????if?(e.next?==?null)
???????????????????????//?鏈表只有一個節(jié)點,直接賦值
???????????????????????//為什么要重新Hash呢?因為長度擴大以后,Hash的規(guī)則也隨之改變。
????????????????????????newTab[e.hash?&?(newCap?-?1)]?=?e;
????????????????????else?if?(e?instanceof?TreeNode)
????????????????????????//?e為紅黑樹的情況
????????????????????????((TreeNode)e).split(this,?newTab,?j,?oldCap);
????????????????????else?{?//?preserve?order鏈表優(yōu)化重hash的代碼塊
????????????????????????Node?loHead?=?null,?loTail?=?null;
????????????????????????Node?hiHead?=?null,?hiTail?=?null;
????????????????????????Node?next;
????????????????????????do?{
????????????????????????????next?=?e.next;
????????????????????????????//?原索引
????????????????????????????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.7 remove方法

remove(key) 方法 和 remove(key, value) 方法都是通過調(diào)用removeNode的方法來實現(xiàn)刪除元素的

final?Node?removeNode(int?hash,?Object?key,?Object?value,
???????????????????????????????boolean?matchValue,?boolean?movable)?{
????????Node[]?tab;?Node?p;?int?n,?index;
????????if?((tab?=?table)?!=?null?&&?(n?=?tab.length)?>?0?&&
????????????(p?=?tab[index?=?(n?-?1)?&?hash])?!=?null)?{
????????????Node?node?=?null,?e;?K?k;?V?v;
????????????if?(p.hash?==?hash?&&
????????????????((k?=?p.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
????????????????//?index?元素只有一個元素
????????????????node?=?p;
????????????else?if?((e?=?p.next)?!=?null)?{
????????????????if?(p?instanceof?TreeNode)
????????????????????//?index處是一個紅黑樹
????????????????????node?=?((TreeNode)p).getTreeNode(hash,?key);
????????????????else?{
????????????????????//?index處是一個鏈表,遍歷鏈表返回node
????????????????????do?{
????????????????????????if?(e.hash?==?hash?&&
????????????????????????????((k?=?e.key)?==?key?||
?????????????????????????????(key?!=?null?&&?key.equals(k))))?{
????????????????????????????node?=?e;
????????????????????????????break;
????????????????????????}
????????????????????????p?=?e;
????????????????????}?while?((e?=?e.next)?!=?null);
????????????????}
????????????}
????????????//?分不同情形刪除節(jié)點
????????????if?(node?!=?null?&&?(!matchValue?||?(v?=?node.value)?==?value?||
?????????????????????????????????(value?!=?null?&&?value.equals(v))))?{
????????????????if?(node?instanceof?TreeNode)
????????????????????((TreeNode)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.8 get

/**
???*?函數(shù)原型
???*?作用:根據(jù)鍵key,向HashMap獲取對應(yīng)的值
???*/?
?? map.get(key);
?/**
???*?源碼分析
???*/?
???public?V?get(Object?key)?{
????Node?e;
????//?1\.?計算需獲取數(shù)據(jù)的hash值
????//?2\.?通過getNode()獲取所查詢的數(shù)據(jù)?->>分析1
????//?3\.?獲取后,判斷數(shù)據(jù)是否為空
????return?(e?=?getNode(hash(key),?key))?==?null???null?:?e.value;
}
/**
???*?分析1:getNode(hash(key),?key))
???*/?
final?Node?getNode(int?hash,?Object?key)?{
????Node[]?tab;?Node?first,?e;?int?n;?K?k;
????//?1\.?計算存放在數(shù)組table中的位置
????if?((tab?=?table)?!=?null?&&?(n?=?tab.length)?>?0?&&
????????(first?=?tab[(n?-?1)?&?hash])?!=?null)?{
????????//?4\.?通過該函數(shù),依次在數(shù)組、紅黑樹、鏈表中查找(通過equals()判斷)
????????//?a.?先在數(shù)組中找,若存在,則直接返回
????????if?(first.hash?==?hash?&&?//?always?check?first?node
????????????((k?=?first.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
????????????return?first;
????????//?b.?若數(shù)組中沒有,則到紅黑樹中尋找
????????if?((e?=?first.next)?!=?null)?{
????????????//?在樹中g(shù)et
????????????if?(first?instanceof?TreeNode)
????????????????return?((TreeNode)first).getTreeNode(hash,?key);
????????????//?c.?若紅黑樹中也沒有,則通過遍歷,到鏈表中尋找
????????????do?{
????????????????if?(e.hash?==?hash?&&
????????????????????((k?=?e.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
????????????????????return?e;
????????????}?while?((e?=?e.next)?!=?null);
????????}
????}
????return?null;
}

在JDK1.7及以前的版本中,HashMap里是沒有紅黑樹的實現(xiàn)的,在JDK1.8中加入了紅黑樹是為了防止哈希表碰撞攻擊,當(dāng)鏈表鏈長度為8時,及時轉(zhuǎn)成紅黑樹,提高map的效率

如果某個桶中的記錄過大的話(當(dāng)前是TREEIFY_THRESHOLD = 8),HashMap會動態(tài)的使用一個專門的treemap實現(xiàn)來替換掉它。這樣做的結(jié)果會更好,是O(logn),而不是糟糕的O(n)。它是如何工作的?前面產(chǎn)生沖突的那些KEY對應(yīng)的記錄只是簡單的追加到一個鏈表后面,這些記錄只能通過遍歷來進行查找。但是超過這個閾值后HashMap開始將列表升級成一個二叉樹,使用哈希值作為樹的分支變量,如果兩個哈希值不等,但指向同一個桶的話,較大的那個會插入到右子樹里。如果哈希值相等,HashMap希望key值最好是實現(xiàn)了Comparable接口的,這樣它可以按照順序來進行插入。這對HashMap的key來說并不是必須的,不過如果實現(xiàn)了當(dāng)然最好。如果沒有實現(xiàn)這個接口,在出現(xiàn)嚴(yán)重的哈希碰撞的時候,你就并別指望能獲得性能提升了。

這個性能提升有什么用處?比方說惡意的程序,如果它知道我們用的是哈希算法,它可能會發(fā)送大量的請求,導(dǎo)致產(chǎn)生嚴(yán)重的哈希碰撞。然后不停的訪問這些key就能顯著的影響服務(wù)器的性能,這樣就形成了一次拒絕服務(wù)攻擊(DoS)。JDK 8中從O(n)到O(logn)的飛躍,可以有效地防止類似的攻擊,同時也讓HashMap性能的可預(yù)測性稍微增強了一些

/**
???*?源碼分析:resize(2 * table.length)
???*?作用:當(dāng)容量不足時(容量?>?閾值),則擴容(擴到2倍)
???*/?
???void?resize(int?newCapacity)?{??
????//?1\.?保存舊數(shù)組(old?table)?
????Entry[]?oldTable?=?table;??
????//?2\.?保存舊容量(old?capacity?),即數(shù)組長度
????int?oldCapacity?=?oldTable.length;?
????//?3\.?若舊容量已經(jīng)是系統(tǒng)默認(rèn)最大容量了,那么將閾值設(shè)置成整型的最大值,退出????
????if?(oldCapacity?==?MAXIMUM_CAPACITY)?{??
????????threshold?=?Integer.MAX_VALUE;??
????????return;??
????}??
????//?4\.?根據(jù)新容量(2倍容量)新建1個數(shù)組,即新table??
????Entry[]?newTable?=?new?Entry[newCapacity];??
????//?5\.?(重點分析)將舊數(shù)組上的數(shù)據(jù)(鍵值對)轉(zhuǎn)移到新table中,從而完成擴容?->>分析1.1?
????transfer(newTable);?
????//?6\.?新數(shù)組table引用到HashMap的table屬性上
????table?=?newTable;??
????//?7\.?重新設(shè)置閾值??
????threshold?=?(int)(newCapacity?*?loadFactor);?
}?
?/**
???*?分析1.1:transfer(newTable);?
???*?作用:將舊數(shù)組上的數(shù)據(jù)(鍵值對)轉(zhuǎn)移到新table中,從而完成擴容
???*?過程:按舊鏈表的正序遍歷鏈表、在新鏈表的頭部依次插入
???*/?
void?transfer(Entry[]?newTable)?{
??????//?1\.?src引用了舊數(shù)組
??????Entry[]?src?=?table;?
??????//?2\.?獲取新數(shù)組的大小?=?獲取新容量大小?????????????????
??????int?newCapacity?=?newTable.length;
??????//?3\.?通過遍歷?舊數(shù)組,將舊數(shù)組上的數(shù)據(jù)(鍵值對)轉(zhuǎn)移到新數(shù)組中
??????for?(int?j?=?0;?j???????????//?3.1?取得舊數(shù)組的每個元素??
??????????Entry?e?=?src[j];???????????
??????????if?(e?!=?null)?{
??????????????//?3.2?釋放舊數(shù)組的對象引用(for循環(huán)后,舊數(shù)組不再引用任何對象)
??????????????src[j]?=?null;?
??????????????do?{?
??????????????????//?3.3?遍歷?以該數(shù)組元素為首?的鏈表
??????????????????//?注:轉(zhuǎn)移鏈表時,因是單鏈表,故要保存下1個結(jié)點,否則轉(zhuǎn)移后鏈表會斷開
??????????????????Entry?next?=?e.next;?
?????????????????//?3.3?重新計算每個元素的存儲位置
?????????????????int?i?=?indexFor(e.hash,?newCapacity);?
?????????????????// 3.4 將元素放在數(shù)組上:采用單鏈表的頭插入方式?=?在鏈表頭上存放數(shù)據(jù)?=?將數(shù)組位置的原有數(shù)據(jù)放在后1個指針、將需放入的數(shù)據(jù)放到數(shù)組位置中
?????????????????//?即?擴容后,可能出現(xiàn)逆序:按舊鏈表的正序遍歷鏈表、在新鏈表的頭部依次插入
?????????????????e.next?=?newTable[i];?
?????????????????newTable[i]?=?e;??
?????????????????//?訪問下1個Entry鏈上的元素,如此不斷循環(huán),直到遍歷完該鏈表上的所有節(jié)點
?????????????????e?=?next;?????????????
?????????????}?while?(e?!=?null);
?????????????//?如此不斷循環(huán),直到遍歷完數(shù)組上的所有數(shù)據(jù)元素
?????????}
?????}
?}

從上面可看出:在擴容resize()過程中,在將舊數(shù)組上的數(shù)據(jù) 轉(zhuǎn)移到 新數(shù)組上時,轉(zhuǎn)移數(shù)據(jù)操作 = 按舊鏈表的正序遍歷鏈表、在新鏈表的頭部依次插入,即在轉(zhuǎn)移數(shù)據(jù)、擴容后,容易出現(xiàn)鏈表逆序的情況

設(shè)重新計算存儲位置后不變,即擴容前 = 1->2->3,擴容后 = 3->2->1

此時若并發(fā)執(zhí)行 put 操作,一旦出現(xiàn)擴容情況,則 容易出現(xiàn) 環(huán)形鏈表,從而在獲取數(shù)據(jù)、遍歷鏈表時 形成死循環(huán)(Infinite Loop),即死鎖

單線程rehash

單線程情況下,rehash無問題

多線程并發(fā)下的rehash

這里假設(shè)有兩個線程同時執(zhí)行了put操作并引發(fā)了rehash,執(zhí)行了transfer方法,并假設(shè)線程一進入transfer方法并執(zhí)行完next = e.next后,因為線程調(diào)度所分配時間片用完而“暫停”,此時線程二完成了transfer方法的執(zhí)行。此時狀態(tài)如下。

接著線程1被喚醒,繼續(xù)執(zhí)行第一輪循環(huán)的剩余部分

e.next?=?newTable[1]?=?null
newTable[1]?=?e?=?key(5)
e?=?next?=?key(9)

結(jié)果如下圖所示

接著執(zhí)行下一輪循環(huán),結(jié)果狀態(tài)圖如下所示

繼續(xù)下一輪循環(huán),結(jié)果狀態(tài)圖如下所示

此時循環(huán)鏈表形成,并且key(11)無法加入到線程1的新數(shù)組。在下一次訪問該鏈表時會出現(xiàn)死循環(huán)。

Fast-fail

產(chǎn)生原因

在使用迭代器的過程中如果HashMap被修改,那么ConcurrentModificationException將被拋出,也即Fast-fail策略。

當(dāng)HashMap的iterator()方法被調(diào)用時,會構(gòu)造并返回一個新的EntryIterator對象,并將EntryIterator的expectedModCount設(shè)置為HashMap的modCount(該變量記錄了HashMap被修改的次數(shù))。

HashIterator()?{
??expectedModCount?=?modCount;
??if?(size?>?0)?{?//?advance?to?first?entry
??Entry[]?t?=?table;
??while?(index?????;
??}
}

在通過該Iterator的next方法訪問下一個Entry時,它會先檢查自己的expectedModCount與HashMap的modCount是否相等,如果不相等,說明HashMap被修改,直接拋出ConcurrentModificationException。該Iterator的remove方法也會做類似的檢查。該異常的拋出意在提醒用戶及早意識到線程安全問題。

線程安全解決方案

單線程條件下,為避免出現(xiàn)ConcurrentModificationException,需要保證只通過HashMap本身或者只通過Iterator去修改數(shù)據(jù),不能在Iterator使用結(jié)束之前使用HashMap本身的方法修改數(shù)據(jù)。因為通過Iterator刪除數(shù)據(jù)時,HashMap的modCount和Iterator的expectedModCount都會自增,不影響二者的相等性。如果是增加數(shù)據(jù),只能通過HashMap本身的方法完成,此時如果要繼續(xù)遍歷數(shù)據(jù),需要重新調(diào)用iterator()方法從而重新構(gòu)造出一個新的Iterator,使得新Iterator的expectedModCount與更新后的HashMap的modCount相等。

多線程條件下,可使用Collections.synchronizedMap方法構(gòu)造出一個同步Map,或者直接使用線程安全的ConcurrentHashMap。

總結(jié)

以上是生活随笔為你收集整理的hashmap转红黑树的阈值为8_面试必考的 HashMap,这篇总结到位了的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

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