Java集合-HashMap源碼實(shí)現(xiàn)深入解析

原創(chuàng)作品,轉(zhuǎn)載請注明出處。

概述

本文學(xué)習(xí)知識點(diǎn)

1.HashMap的存儲結(jié)構(gòu)怎么實(shí)現(xiàn),它有什么特點(diǎn)。
2.HashMap的工作原理。
3.put和get方法實(shí)現(xiàn)源碼分析。
4.hash值有什么作用?如何進(jìn)行hash?equals和hashCode方法有什么作用?
5.何謂負(fù)載因子,有什么作用?
6.何時會觸發(fā)擴(kuò)容,以及如何擴(kuò)容?

    Map<String, String> map = new HashMap<String, String>();
    map.put("liuyi", "劉一");
    map.put("wanger", "王二");
    map.put("zhangsan", "張三");
    map.put("lisi", "李四");
    map.put("wangwu", "王五");
    map.put("zhaoliu", "趙六");
    map.put("chenqi", "陳七");
    map.put("yangba", "楊八");
    map.put("zhoujiu", "周九");
    for (Map.Entry<String, String> entry : map.entrySet()) {
        System.out.println(entry.getValue() + "\t" + entry.getKey());
    }

想想上面代碼的輸出結(jié)果是什么樣?運(yùn)行后,可能的結(jié)果是:

趙六 zhaoliu
劉一 liuyi
王五 wangwu
王二 wanger
陳七 chenqi
李四 lisi
周九 zhoujiu
張三 zhangsan
楊八 yangba

從這個結(jié)果可以窺探出,HashMap存儲的順序和迭代時讀取的順序不相同,并非有序存儲。來看看底層如何存儲,如下圖為Eclipse debug時的存儲結(jié)構(gòu):

調(diào)試時HashMap的存儲結(jié)構(gòu)

從圖中來看,HashMap內(nèi)部用一個table(Entry元素數(shù)組)來存儲元素,索引中的每個元素又采用鏈表的結(jié)構(gòu)進(jìn)行存儲,用于存儲相同索引的元素。其中Entry類就是一個節(jié)點(diǎn)類,具有鏈接下一個節(jié)點(diǎn)的功能,內(nèi)部存儲了元素的key,value,hash,next引用,Entry的結(jié)構(gòu)如下,采用內(nèi)部類的方式實(shí)現(xiàn):

內(nèi)部類:元素節(jié)點(diǎn)Entry的結(jié)構(gòu)

HashMap的特性

1.實(shí)現(xiàn)了Map接口,里面的方法全部被HashMap實(shí)現(xiàn)。
2.允許null鍵和null值。這點(diǎn)和Hashtable不同,Hashtable不允許。
3.非線程安全,這點(diǎn)也和Hashtable不同,Hashtable為線程安全的。
4.不保證有序,即元素的插入和讀取順序不保證一致,這一點(diǎn)從開始的示例程序和圖就能看出來。
5.也不保證元素的順序始終不變,在擴(kuò)容時,元素的順序會重新被打亂。

有兩個參數(shù)能夠影響HashMap的結(jié)構(gòu):

1.initial capacity:table的初始容量(并非存放元素節(jié)點(diǎn)的容量大小,僅僅是table表的大?。J(rèn)初始容量為16。
2.load factor:負(fù)載因子,用于確定table的容量達(dá)到多大比例時,對容量進(jìn)行擴(kuò)容的一個參數(shù)。默認(rèn)值為0.75,即默認(rèn)超過12個時開始擴(kuò)容,擴(kuò)容時,會重新計算所有元素的hash值(rehashed),以確定其在新table上的索引下標(biāo),因此,擴(kuò)容會改變原有hashtable的結(jié)構(gòu),元素的順序也會重新改變。每次擴(kuò)容后,容量的大小都是原來的2倍。至于為什么默認(rèn)負(fù)載因子設(shè)置為0.75,按照官方的API說法,是因?yàn)橥ǔ?.75能夠在時間復(fù)雜度和空間復(fù)雜度上達(dá)到最好的平衡效果。在設(shè)置初始容量時應(yīng)該考慮到所需的條目數(shù)及其加載因子,以便最大限度地減少 rehash 操作次數(shù)。

put方法的實(shí)現(xiàn) JDK1.7

put方法的步驟:
1.對key的hashcode值再計算hash。
2.用計算出的hash值,與表的length求&算出索引位置。
3.如果索引碰撞,遍歷鏈表,判斷是否有key存在,若存在更新value值。(不過在JDK1.8中鏈表元素超過8個時,會把鏈表結(jié)構(gòu)轉(zhuǎn)換成紅黑樹結(jié)構(gòu)存儲,提高查詢的性能)
4.若未碰撞,直接放入表中。
5.放入前,如果元素個數(shù)超過負(fù)載因子的比例,則進(jìn)行rehash,擴(kuò)容,之會插入。
6.null key的元素永遠(yuǎn)會放到index為0的鏈表里面。

代碼實(shí)現(xiàn)如下:
put方法實(shí)現(xiàn)
插入新元素時會判斷是否進(jìn)行擴(kuò)容,每次擴(kuò)容1倍

get方法實(shí)現(xiàn)

步驟:
1.null key走特殊邏輯。
2.key的hashcode計算hash值。
3.hash值計算表索引。
4.遍歷索引下的鏈表,找到就返回,找不到返回null。

get方法

hash方法的實(shí)現(xiàn)

如下為hash方法的實(shí)現(xiàn)源碼:
hash方法

hash方法中:對key的hashCode值進(jìn)行了異或,邏輯右移等多個操作,目的是盡量減小沖突。因?yàn)閠able的容量是2的冪,直接用hashcode,低位會很容易碰撞,因此做了多次移位和異或操作,保證高位和低位都參與到hash的計算中,減少碰撞。

計算下標(biāo)的時候,是這樣實(shí)現(xiàn)的(使用&位操作,而非%求余):
hash & (length - 1)
設(shè)計者認(rèn)為這方法很容易發(fā)生碰撞。為什么這么說呢?不妨思考一下,在n - 1為15(0x1111)時,其實(shí)散列真正生效的只是低4bit的有效位,當(dāng)然容易碰撞了。
時間復(fù)雜度:讀取O(1) + O(n),1為找索引的時間,n為鏈表元素的個數(shù)。插入O(1)。所以,如果沖突很頻繁,某些鏈表非常長,就會影響HashMap的讀取速度。這一點(diǎn)在JDK1.8中進(jìn)行了性能提升,鏈表長度超過8則改為紅黑樹的存儲結(jié)構(gòu)實(shí)現(xiàn)。

resize擴(kuò)容實(shí)現(xiàn)

實(shí)現(xiàn)如下:
image.png

步驟:
1.創(chuàng)建新的table。
2.將舊table的元素重新計算hash,根據(jù)hash計算新table的索引,插入新table。
3.新table覆蓋原table的引用,更新threshold值為新table * 負(fù)載因子的大小。

總結(jié)


讓我們來回答幾個問題,以加深對HashMap的理解:

1. 什么時候會使用HashMap?他有什么特點(diǎn)?

是基于Map接口的實(shí)現(xiàn),存儲鍵值對時,它可以接收null的鍵值,是非同步的,HashMap存儲著Entry(hash, key, value, next)對象。

2. 你知道HashMap的工作原理嗎?

通過put和get存儲和獲取對象。存儲對象時,我們將K/V傳給put方法時,它調(diào)用hashCode計算hash從而得到table位置,進(jìn)一步存儲,HashMap會根據(jù)當(dāng)前table的占用情況自動調(diào)整容量(超過Load Facotr則resize為原來的2倍)。獲取對象時,我們將K傳給get,它調(diào)用hashCode計算hash從而得到table位置,并進(jìn)一步調(diào)用equals()方法確定鍵值對。如果發(fā)生碰撞的時候,Hashmap通過鏈表將產(chǎn)生碰撞沖突的元素組織起來,在Java 8中,如果一個table中碰撞沖突的元素超過某個限制(默認(rèn)是8),則使用紅黑樹來替換鏈表,從而提高速度。

3. 你知道get和put的原理嗎?equals()和hashCode()的都有什么作用?

通過對key的hashCode()進(jìn)行hashing,并計算下標(biāo)( n-1 & hash),從而獲得buckets的位置。如果產(chǎn)生碰撞,則利用key.equals()方法去鏈表或樹中去查找對應(yīng)的節(jié)點(diǎn)

4. 你知道hash的實(shí)現(xiàn)嗎?為什么要這樣實(shí)現(xiàn)?

在Java 1.8的實(shí)現(xiàn)中,是通過hashCode()的高16位異或低16位實(shí)現(xiàn)的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質(zhì)量來考慮的,這么做可以在bucket的n比較小的時候,也能保證考慮到高低bit都參與到hash的計算中,同時不會有太大的開銷。

5. 何時會觸發(fā)擴(kuò)容,以及如何擴(kuò)容?

如果table超過了負(fù)載因子(默認(rèn)0.75)比例的容量,則會重新resize一個原來長度兩倍的HashMap,并且重新調(diào)用hash方法計算元素在新table的索引,并挨個插入。


白天在加班加點(diǎn)的干活,還要抽出時間成為技術(shù)知識的分享者,分享不易。
喜歡我的文章請點(diǎn)贊,歡迎打賞,成為我們堅持的動力。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容