Java HashMap原理解析

本文分析HashMap的實(shí)現(xiàn)原理定踱。

數(shù)據(jù)結(jié)構(gòu)(散列表)

HashMap是一個(gè)散列表(也叫哈希表),用來存儲(chǔ)鍵值對(duì)(key-value)映射翩概。散列表是一種數(shù)組和鏈表的結(jié)合體汪疮,結(jié)構(gòu)圖如下:

來自百度百科的哈希表結(jié)構(gòu)圖

簡(jiǎn)單來說散列表就是一個(gè)數(shù)組(上圖縱向),數(shù)組的每個(gè)元素是一個(gè)鏈表(上圖橫向)烂叔,類似二維數(shù)組谨胞。鏈表的每個(gè)節(jié)點(diǎn)就是我們存儲(chǔ)的key-value數(shù)據(jù)(源碼中將key和value封裝成Entry對(duì)象作為鏈表的節(jié)點(diǎn))固歪。


哈希算法

對(duì)于散列表蒜鸡,不管是存值還是取值胯努,都需要通過Key來定位散列表中的一個(gè)具體的位置(即某個(gè)鏈表的某個(gè)節(jié)點(diǎn)),計(jì)算這個(gè)位置的方法就是哈希算法逢防。

大概過程是這樣的:

  1. 用Key的hash值對(duì)數(shù)組長(zhǎng)度做取余操作得到一個(gè)整數(shù)叶沛,這個(gè)整數(shù)作為數(shù)組中的索引得到這個(gè)索引位置的鏈表。
  2. 得到鏈表之后忘朝,就可以存值和取值了灰署。
    如果是存值,直接把數(shù)據(jù)插入到鏈表的頭部或者尾部即可(或者已存在就替換)局嘁;
    如果是取值溉箕,就遍歷鏈表,通過key的equals方法找到具體的節(jié)點(diǎn)悦昵。

例如一個(gè)key-value對(duì)要存到上圖的散列表里肴茄,假設(shè)key的哈希值是17,由圖可知(縱向)數(shù)組長(zhǎng)度是16但指,那么17對(duì)16取余結(jié)果是1寡痰,數(shù)組中索引1位置的鏈表是 1->337->353 ,所以這個(gè)key-value對(duì)存儲(chǔ)到這個(gè)鏈表里面(插到頭還是尾可能不同Java版本不一樣)棋凳。如果是取值拦坠,就遍歷這個(gè)鏈表,由于這個(gè)鏈表每個(gè)節(jié)點(diǎn)的key的哈希值都一樣剩岳,所以根據(jù)equals方法來確定具體是哪個(gè)節(jié)點(diǎn)贞滨。

通過上面的哈希算法,可以有如下結(jié)論:

  • 不同的key具體相同的哈希值叫做哈希沖突拍棕,HashMap解決哈希沖突的方法是鏈表法疲迂,將具有相同哈希值的key放在同一個(gè)鏈表中,然后利用key類的equals方法來確定具體是哪個(gè)節(jié)點(diǎn)莫湘。
  • Key的唯一性是通過哈希值和equals方法共同決定的尤蒿,所以想要用一個(gè)類作為HashMap的鍵,必須重寫這個(gè)類的hashCode和equals方法幅垮。同理腰池,HashSet是基于HashMap實(shí)現(xiàn)的,它沒有重復(fù)元素的特點(diǎn)是利用HashMap沒有重復(fù)鍵實(shí)現(xiàn)的忙芒。所以示弓,Set集合里面的元素類,也必須同時(shí)實(shí)現(xiàn)hashCode方法和equals方法呵萨。
  • HashMap存儲(chǔ)的數(shù)據(jù)是無序的奏属。


為什么HashMap大小是2的整數(shù)次冪的時(shí)候效率最高

哈希算法主要分兩步操作:1.通過哈希值定位一個(gè)鏈表; 2.遍歷鏈表潮峦,通過equals方法找到具體節(jié)點(diǎn)囱皿。為了使哈希算法效率最高勇婴,應(yīng)該盡量讓數(shù)據(jù)在哈希表中均勻分布,因?yàn)槟菢涌梢员苊獬霈F(xiàn)過長(zhǎng)的鏈表嘱腥,也就降低了遍歷鏈表的代價(jià)耕渴。
如何保證均勻分布?前面的哈希算法說到齿兔,通過取余操作將Key的哈希值轉(zhuǎn)換成數(shù)組下標(biāo)橱脸,這樣可以認(rèn)為是均勻的。但是分苇,源碼中并沒有直接用%操作符取余添诉,而是使用了更高效的與運(yùn)算,源碼如下:

/**
 * Returns index for hash code h.
 */
static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}

這樣就多了一些限制医寿,因?yàn)橹挥挟?dāng)length是2的整數(shù)次冪的時(shí)候吻商,h & (length-1) = h % length才成立。當(dāng)然糟红,如果length不是2的整數(shù)次冪艾帐,h & (length-1)的結(jié)果也一定比length小,將Key轉(zhuǎn)換成數(shù)組下標(biāo)也沒什么問題盆偿,但是柒爸,這樣會(huì)導(dǎo)致元素分布不均勻嚴(yán)重影響散列表的訪問效率∈屡ぃ看下面的一個(gè)示例代碼:

示例代碼

解釋一下圖中的代碼捎稚,隨機(jī)生成一組Key,然后利用與運(yùn)算求橄,把key全部轉(zhuǎn)換成一個(gè)數(shù)組容量的索引今野,這樣就得到一組索引值,這組索引中不相同的值越多罐农,說明分布越均勻条霜,輸出結(jié)果的result就是 “這組索引中不相同的值的數(shù)量”。
從運(yùn)行結(jié)果來看涵亏,容量是64的時(shí)候相比于其他幾個(gè)容量大小宰睡,分布是最均勻的。容量是65的時(shí)候气筋,每次結(jié)果都是2拆内,原因很簡(jiǎn)單,當(dāng)容量是65的時(shí)候宠默,下標(biāo)=h&64麸恍,64的二進(jìn)制是1000000,很明顯搀矫,與它進(jìn)行與運(yùn)算的結(jié)果只有兩種情況抹沪,0和64刻肄,也就是說,如果HashMap大小被指定成65采够,對(duì)于任意Key肄方,只會(huì)存儲(chǔ)到散列表數(shù)組的第0個(gè)或第64個(gè)鏈表中冰垄,浪費(fèi)了63個(gè)空間蹬癌,同時(shí)也導(dǎo)致0和64兩個(gè)鏈表過長(zhǎng),取值的時(shí)候遍歷鏈表的代價(jià)很高虹茶。容量66和67的結(jié)果是4同理逝薪。如果容量是64,那么下標(biāo)=h&63蝴罪,63的二進(jìn)制是111111董济,每一位都是1,好處就是對(duì)于任意Key要门,與63做與運(yùn)算的結(jié)果可能是1-63的任意數(shù)虏肾,很多Key的話自然就能分布均勻。
通過這個(gè)示例代碼的分析就可以找到一個(gè)規(guī)律了欢搜,容量length=2^n 是分布最均勻封豪,因?yàn)閘ength-1的二進(jìn)制每一位都是1;相反的length=2^n+1是分布最不均勻的炒瘟,因?yàn)閘ength-1的二進(jìn)制中的1數(shù)量最少吹埠。

結(jié)論:HashMap大小是2的整數(shù)次冪的時(shí)候效率最高,因?yàn)檫@個(gè)時(shí)候元素在散列表中的分布最均勻疮装。

從上面的分析來看缘琅,使用與運(yùn)算雖然效率高了,但是增加了使用限制廓推,如果用%取余的做法刷袍,那么對(duì)于任何大小的容量都能做到均勻分布,可以把圖中代碼int a = keySet[j] & (c - 1); 改成 int a = keySet[j] % c;試一下樊展。


HashMap的容量

通過上面的分析做个,容量是2的整數(shù)次冪的時(shí)候效率最高,那么很容易想到滚局,如果隨著數(shù)據(jù)量的增長(zhǎng)居暖,HashMap需要擴(kuò)容的時(shí)候是2倍擴(kuò)容,區(qū)別于ArrayList的1.5倍擴(kuò)容藤肢。
那么什么時(shí)候擴(kuò)容呢太闺?首先說明一下,我們所說的HashMap的容量是指散列表中數(shù)組的大小嘁圈,這個(gè)大小不能決定HashMap能存多少數(shù)據(jù)省骂,因?yàn)橹灰湵碜銐蜷L(zhǎng)蟀淮,存多少數(shù)據(jù)都沒問題。但是钞澳,數(shù)據(jù)量很大的時(shí)候怠惶,如果數(shù)組太小,就會(huì)導(dǎo)致鏈表很長(zhǎng)轧粟,get元素的效率就會(huì)降低策治,所以我們應(yīng)該在適當(dāng)?shù)臅r(shí)候擴(kuò)容。源碼默認(rèn)的做法是兰吟,當(dāng)數(shù)據(jù)量達(dá)到容量的75%的時(shí)候擴(kuò)容通惫,這個(gè)值稱為負(fù)載因子,75%應(yīng)該是大量實(shí)驗(yàn)后統(tǒng)計(jì)得到的最優(yōu)值混蔼,沒有特殊情況不要通過構(gòu)造方法指定為其他值履腋。
擴(kuò)容是有代價(jià)了,會(huì)導(dǎo)致所有已存的數(shù)據(jù)重新計(jì)算位置惭嚣,所以遵湖,和ArrayList一樣,當(dāng)知道大概的數(shù)據(jù)量的時(shí)候晚吞,可以指定HashMap的大小盡量避免擴(kuò)容延旧,指定大小要注意75%這個(gè)負(fù)載因子,比如數(shù)據(jù)量是63個(gè)的話载矿,HashMap的大小應(yīng)該是128而不是64垄潮。

對(duì)于容量的計(jì)算,源碼已經(jīng)封裝好了一個(gè)方法

/**
 * Returns a power of two size for the given target capacity.
 */
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;
}

此方法在HashMap的構(gòu)造方法中被調(diào)用闷盔,所以指定容量的時(shí)候無需自己計(jì)算弯洗,比如數(shù)據(jù)量是63,直接new HashMap<>(63)即可逢勾。


HashMap的遍歷

前面提到一點(diǎn)牡整,散列表中的鏈表的節(jié)點(diǎn)是Entry對(duì)象,通過Entry對(duì)象可以得到Key和Value溺拱。HashMap的遍歷方法有很多逃贝,大概可以分為3種,分別是通過map.entrySet()迫摔、map.keySet()沐扳、map.values()三種方式遍歷。比較效率的話句占,map.values()方式無法得到key沪摄,這里不考慮。比較map.entrySet()和map.keySet()的話,結(jié)合散列表的結(jié)構(gòu)特點(diǎn)杨拐,很明顯map.entrySet()直接遍歷Entry集合(所有鏈表節(jié)點(diǎn))取出Key和Value即可(一次循環(huán))祈餐,map.keySet()遍歷的是Key,得到Key之后在通過Key去遍歷相應(yīng)的鏈表找到具體的節(jié)點(diǎn)(多個(gè)循環(huán))哄陶,所以前者效率高帆阳。


擴(kuò)展:LinkedHashMap和LruCatch

對(duì)于LinkedHashMap的理解,我覺得一張圖就夠了:

LinkedHashMap結(jié)構(gòu)圖

在散列表的基礎(chǔ)上加上了雙向循環(huán)鏈表(圖中黃色箭頭和綠色箭頭)屋吨,所以可以拆分成一個(gè)散列表和一個(gè)雙向鏈表蜒谤,雙向鏈表如下:

雙向循環(huán)鏈表圖

上面兩張圖片來自:https://www.cnblogs.com/xiaoxi/p/6170590.html

然后使用散列表操作數(shù)據(jù),使用雙向循環(huán)鏈表維護(hù)順序离赫,就實(shí)現(xiàn)了LinkedHashMap芭逝。

LinkedHashMap有一個(gè)屬性可以設(shè)置兩種排序方式:

private final boolean accessOrder;

false表示插入順序塌碌,true表示最近最少使用次序渊胸,后者就是LruCatch的實(shí)現(xiàn)原理。

LinkedHashMap和LruCatch的具體實(shí)現(xiàn)細(xì)節(jié)這里就不分析了台妆。


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末翎猛,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子接剩,更是在濱河造成了極大的恐慌切厘,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,576評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件懊缺,死亡現(xiàn)場(chǎng)離奇詭異疫稿,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)鹃两,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門遗座,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人俊扳,你說我怎么就攤上這事途蒋。” “怎么了馋记?”我有些...
    開封第一講書人閱讀 168,017評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵号坡,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我梯醒,道長(zhǎng)宽堆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,626評(píng)論 1 296
  • 正文 為了忘掉前任茸习,我火速辦了婚禮畜隶,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己代箭,他們只是感情好墩划,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,625評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著嗡综,像睡著了一般乙帮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上极景,一...
    開封第一講書人閱讀 52,255評(píng)論 1 308
  • 那天察净,我揣著相機(jī)與錄音,去河邊找鬼盼樟。 笑死氢卡,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的晨缴。 我是一名探鬼主播译秦,決...
    沈念sama閱讀 40,825評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼击碗!你這毒婦竟也來了筑悴?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,729評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤稍途,失蹤者是張志新(化名)和其女友劉穎阁吝,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體械拍,經(jīng)...
    沈念sama閱讀 46,271評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡突勇,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,363評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了坷虑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片甲馋。...
    茶點(diǎn)故事閱讀 40,498評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖猖吴,靈堂內(nèi)的尸體忽然破棺而出摔刁,到底是詐尸還是另有隱情,我是刑警寧澤海蔽,帶...
    沈念sama閱讀 36,183評(píng)論 5 350
  • 正文 年R本政府宣布共屈,位于F島的核電站,受9級(jí)特大地震影響党窜,放射性物質(zhì)發(fā)生泄漏拗引。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,867評(píng)論 3 333
  • 文/蒙蒙 一幌衣、第九天 我趴在偏房一處隱蔽的房頂上張望矾削。 院中可真熱鬧壤玫,春花似錦、人聲如沸哼凯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽断部。三九已至猎贴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蝴光,已是汗流浹背她渴。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蔑祟,地道東北人趁耗。 一個(gè)月前我還...
    沈念sama閱讀 48,906評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像疆虚,于是被迫代替她去往敵國(guó)和親苛败。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,507評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • HashMap 是 Java 面試必考的知識(shí)點(diǎn)装蓬,面試官?gòu)倪@個(gè)小知識(shí)點(diǎn)就可以了解我們對(duì) Java 基礎(chǔ)的掌握程度著拭。網(wǎng)...
    野狗子嗷嗷嗷閱讀 6,669評(píng)論 9 107
  • 前言 今天來介紹下HashMap纱扭,之前的List牍帚,講了ArrayList、LinkedList乳蛾,就前兩者而言暗赶,反映...
    嘟爺MD閱讀 2,881評(píng)論 2 56
  • 曾經(jīng)天真的以為會(huì)是他手心里的寶、以為是那個(gè)可以隨時(shí)安心退嘁叮靠的港灣蹂随、以為會(huì)是一輩子的依靠……那么多的以為終究只是一廂...
    獨(dú)孤晚晴閱讀 292評(píng)論 0 1
  • 月亮沒那么圓了 月餅沒那么甜了 人兒都走散了 故鄉(xiāng)的天不見了 故鄉(xiāng)的人別離了 別了只有再見了 再見又該很久了
    L志華閱讀 135評(píng)論 0 0
  • 前幾天朋友倩向我抱怨一位追求者的行動(dòng)讓她煩惱不已蹦魔。原來那男生從她高中開始喜歡她激率,現(xiàn)在大學(xué)又在同一個(gè)城市,于是便一直...
    snowinglemon閱讀 545評(píng)論 0 4