【JAVA】HashMap

1苗踪、hashmap 的數(shù)據(jù)結(jié)構(gòu)

要知道 hashmap 是什么,首先要搞清楚它的數(shù)據(jù)結(jié)構(gòu)荠割,在 java 編程語(yǔ)言中,最基本的結(jié)構(gòu)就是兩種旺矾,一個(gè)是數(shù)組蔑鹦,另外一個(gè)是模擬指針(引用),所有的數(shù)據(jù)結(jié)構(gòu)都可以用這兩個(gè)基本結(jié)構(gòu)來(lái)構(gòu)造的箕宙,hashmap 也不例外嚎朽。Hashmap 實(shí)際上是一個(gè)數(shù)組和鏈表的結(jié)合體(在數(shù)據(jù)結(jié)構(gòu)中,一般稱(chēng)之為 “鏈表散列 “)柬帕,請(qǐng)看下圖(橫排表示數(shù)組哟忍,縱排表示數(shù)組元素【實(shí)際上是一個(gè)鏈表】)。

構(gòu)造圖

從圖中我們可以看到一個(gè) hashmap 就是一個(gè)數(shù)組結(jié)構(gòu)陷寝,當(dāng)新建一個(gè) hashmap 的時(shí)候锅很,就會(huì)初始化一個(gè)數(shù)組。我們來(lái)看看 java 代碼:

    /** 
     * The table, resized as necessary. Length MUST Always be a power of two. 
     *  FIXME 這里需要注意這句話凤跑,至于原因后面會(huì)講到 
     */  
    transient Entry[] table;  

    static class Entry<K,V> implements Map.Entry<K,V> {  
        final K key;  
        V value;  
        final int hash;  
        Entry<K,V> next;  
    ..........  
    }  

上面的 Entry 就是數(shù)組中的元素爆安,它持有一個(gè)指向下一個(gè)元素的引用,這就構(gòu)成了鏈表仔引。

當(dāng)我們往 hashmap 中 put 元素的時(shí)候扔仓,先根據(jù) key 的 hash 值得到這個(gè)元素在數(shù)組中的位置(即下標(biāo)),然后就可以把這個(gè)元素放到對(duì)應(yīng)的位置中了咖耘。如果這個(gè)元素所在的位子上已經(jīng)存放有其他元素了翘簇,那么在同一個(gè)位子上的元素將以鏈表的形式存放,新加入的放在鏈頭儿倒,最先加入的放在鏈尾版保。從 hashmap 中 get 元素時(shí),首先計(jì)算 key 的 hashcode,找到數(shù)組中對(duì)應(yīng)位置的某一元素找筝,然后通過(guò) key 的 equals 方法在對(duì)應(yīng)位置的鏈表中找到需要的元素蹈垢。從這里我們可以想象得到,如果每個(gè)位置上的鏈表只有一個(gè)元素袖裕,那么 hashmap 的 get 效率將是最高的,但是理想總是美好的溉瓶,現(xiàn)實(shí)總是有困難需要我們?nèi)タ朔宾?/p>

2、hash 算法

我們可以看到在 hashmap 中要找到某個(gè)元素堰酿,需要根據(jù) key 的 hash 值來(lái)求得對(duì)應(yīng)數(shù)組中的位置疾宏。如何計(jì)算這個(gè)位置就是 hash 算法。前面說(shuō)過(guò) hashmap 的數(shù)據(jù)結(jié)構(gòu)是數(shù)組和鏈表的結(jié)合触创,所以我們當(dāng)然希望這個(gè) hashmap 里面的元素位置盡量的分布均勻些坎藐,盡量使得每個(gè)位置上的元素?cái)?shù)量只有一個(gè),那么當(dāng)我們用 hash 算法求得這個(gè)位置的時(shí)候哼绑,馬上就可以知道對(duì)應(yīng)位置的元素就是我們要的岩馍,而不用再去遍歷鏈表。

所以我們首先想到的就是把 hashcode 對(duì)數(shù)組長(zhǎng)度取模運(yùn)算抖韩,這樣一來(lái)蛀恩,元素的分布相對(duì)來(lái)說(shuō)是比較均勻的。但是茂浮,“乃唬” 運(yùn)算的消耗還是比較大的,能不能找一種更快速席揽,消耗更小的方式那顽馋?java 中時(shí)這樣做的,

    static int indexFor(int h, int length) {  
       return h & (length-1);  
   }  

首先算得 key 得 hashcode 值幌羞,然后跟數(shù)組的長(zhǎng)度 - 1 做一次 “與” 運(yùn)算(&)寸谜。看上去很簡(jiǎn)單新翎,其實(shí)比較有玄機(jī)程帕。比如數(shù)組的長(zhǎng)度是 2 的 4 次方,那么 hashcode 就會(huì)和 2 的 4 次方 - 1 做 “與” 運(yùn)算地啰。很多人都有這個(gè)疑問(wèn)愁拭,為什么 hashmap 的數(shù)組初始化大小都是 2 的次方大小時(shí),hashmap 的效率最高亏吝,我以 2 的 4 次方舉例岭埠,來(lái)解釋一下為什么數(shù)組大小為 2 的冪時(shí) hashmap 訪問(wèn)的性能最高。

看下圖,左邊兩組是數(shù)組長(zhǎng)度為 16(2 的 4 次方)惜论,右邊兩組是數(shù)組長(zhǎng)度為 15许赃。兩組的 hashcode 均為 8 和 9,但是很明顯馆类,當(dāng)它們和 1110 “與” 的時(shí)候混聊,產(chǎn)生了相同的結(jié)果,也就是說(shuō)它們會(huì)定位到數(shù)組中的同一個(gè)位置上去乾巧,這就產(chǎn)生了碰撞句喜,8 和 9 會(huì)被放到同一個(gè)鏈表上,那么查詢(xún)的時(shí)候就需要遍歷這個(gè)鏈表沟于,得到 8 或者 9咳胃,這樣就降低了查詢(xún)的效率。同時(shí)旷太,我們也可以發(fā)現(xiàn)展懈,當(dāng)數(shù)組長(zhǎng)度為 15 的時(shí)候,hashcode 的值會(huì)與 14(1110)進(jìn)行 “與”供璧,那么最后一位永遠(yuǎn)是 0存崖,而 0001,0011嗜傅,0101金句,1001,1011吕嘀,0111违寞,1101 這幾個(gè)位置永遠(yuǎn)都不能存放元素了,空間浪費(fèi)相當(dāng)大偶房,更糟的是這種情況中趁曼,數(shù)組可以使用的位置比數(shù)組長(zhǎng)度小了很多,這意味著進(jìn)一步增加了碰撞的幾率棕洋,減慢了查詢(xún)的效率挡闰!

在這里插入圖片描述

所以說(shuō),當(dāng)數(shù)組長(zhǎng)度為 2 的 n 次冪的時(shí)候掰盘,不同的 key 算得得 index 相同的幾率較小摄悯,那么數(shù)據(jù)在數(shù)組上分布就比較均勻,也就是說(shuō)碰撞的幾率小愧捕,相對(duì)的奢驯,查詢(xún)的時(shí)候就不用遍歷某個(gè)位置上的鏈表,這樣查詢(xún)效率也就較高了次绘。

說(shuō)到這里瘪阁,我們?cè)倩仡^看一下 hashmap 中默認(rèn)的數(shù)組大小是多少撒遣,查看源代碼可以得知是 16,為什么是 16管跺,而不是 15义黎,也不是 20 呢,看到上面 annegu 的解釋之后我們就清楚了吧豁跑,顯然是因?yàn)?16 是 2 的整數(shù)次冪的原因廉涕,16-1的二進(jìn)制是 1 1 1 1 ,能夠盡量報(bào)錯(cuò)hash值的結(jié)果艇拍,在小數(shù)據(jù)量的情況下 16 比 15 和 20 更能減少 key 之間的碰撞火的,而加快查詢(xún)的效率。

所以淑倾,在存儲(chǔ)大容量數(shù)據(jù)的時(shí)候,最好預(yù)先指定 hashmap 的 size 為 2 的整數(shù)次冪次方征椒。就算不指定的話娇哆,也會(huì)以大于且最接近指定值大小的 2 次冪來(lái)初始化的,代碼如下 (HashMap 的構(gòu)造方法中):

// Find a power of 2 >= initialCapacity  
        int capacity = 1;  
        while (capacity < initialCapacity)   
            capacity <<= 1;  

3勃救、hashmap 的 resize

當(dāng) hashmap 中的元素越來(lái)越多的時(shí)候碍讨,碰撞的幾率也就越來(lái)越高(因?yàn)閿?shù)組的長(zhǎng)度是固定的),所以為了提高查詢(xún)的效率蒙秒,就要對(duì) hashmap 的數(shù)組進(jìn)行擴(kuò)容勃黍,數(shù)組擴(kuò)容這個(gè)操作也會(huì)出現(xiàn)在 ArrayList 中,所以這是一個(gè)通用的操作晕讲,很多人對(duì)它的性能表示過(guò)懷疑覆获,不過(guò)想想我們的 “均攤” 原理,就釋然了瓢省,而在 hashmap 數(shù)組擴(kuò)容之后弄息,最消耗性能的點(diǎn)就出現(xiàn)了:原數(shù)組中的數(shù)據(jù)必須重新計(jì)算其在新數(shù)組中的位置,并放進(jìn)去勤婚,這就是 resize摹量。

那么 hashmap 什么時(shí)候進(jìn)行擴(kuò)容呢?當(dāng) hashmap 中的元素個(gè)數(shù)超過(guò)數(shù)組大小 * loadFactor 時(shí)馒胆,就會(huì)進(jìn)行數(shù)組擴(kuò)容缨称,loadFactor 的默認(rèn)值為 0.75,也就是說(shuō),默認(rèn)情況下煮嫌,數(shù)組大小為 16宇攻,那么當(dāng) hashmap 中元素個(gè)數(shù)超過(guò) 16*0.75=12 的時(shí)候,就把數(shù)組的大小擴(kuò)展為 2*16=32骂删,即擴(kuò)大一倍掌动,然后重新計(jì)算每個(gè)元素在數(shù)組中的位置,而這是一個(gè)非常消耗性能的操作宁玫,所以如果我們已經(jīng)預(yù)知 hashmap 中元素的個(gè)數(shù)粗恢,那么預(yù)設(shè)元素的個(gè)數(shù)能夠有效的提高 hashmap 的性能。比如說(shuō)欧瘪,我們有 1000 個(gè)元素 new HashMap (1000), 但是理論上來(lái)講 new HashMap (1024) 更合適眷射,不過(guò)上面 annegu 已經(jīng)說(shuō)過(guò),即使是 1000佛掖,hashmap 也自動(dòng)會(huì)將其設(shè)置為 1024妖碉。 但是 new HashMap (1024) 還不是更合適的,因?yàn)?0.75*1000 < 1000, 也就是說(shuō)為了讓 0.75 * size > 1000, 我們必須這樣 new HashMap (2048) 才最合適芥被,既考慮了 & 的問(wèn)題欧宜,也避免了 resize 的問(wèn)題。

  • 如果需要擴(kuò)容拴魄,調(diào)用擴(kuò)容的方法 resize ()

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    //判斷是否有超出擴(kuò)容的最大值冗茸,如果達(dá)到最大值則不進(jìn)行擴(kuò)容操作
    if (oldCapacity == MAXIMUM_CAPACITY) {
      threshold = Integer.MAX_VALUE;
      return;
    }
 
    Entry[] newTable = new Entry[newCapacity];
    // transfer()方法把原數(shù)組中的值放到新數(shù)組中
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    //設(shè)置hashmap擴(kuò)容后為新的數(shù)組引用
    table = newTable;
    //設(shè)置hashmap擴(kuò)容新的閾值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
  }
  • transfer () 在實(shí)際擴(kuò)容時(shí)候把原來(lái)數(shù)組中的元素放入新的數(shù)組中

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    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);
        }
        //通過(guò)key值的hash值和新數(shù)組的大小算出在當(dāng)前數(shù)組中的存放位置
        int i = indexFor(e.hash, newCapacity);
        e.next = newTable[i];
        newTable[i] = e;
        e = next;
      }
    }
  }

4、key 的 hashcode 與 equals 方法改寫(xiě)

在第一部分 hashmap 的數(shù)據(jù)結(jié)構(gòu)中匹中,就寫(xiě)了 get 方法的過(guò)程:首先計(jì)算 key 的 hashcode夏漱,找到數(shù)組中對(duì)應(yīng)位置的某一元素,然后通過(guò) key 的 equals 方法在對(duì)應(yīng)位置的鏈表中找到需要的元素顶捷。所以挂绰,hashcode 與 equals 方法對(duì)于找到對(duì)應(yīng)元素是兩個(gè)關(guān)鍵方法。

Hashmap 的 key 可以是任何類(lèi)型的對(duì)象服赎,例如 User 這種對(duì)象葵蒂,為了保證兩個(gè)具有相同屬性的 user 的 hashcode 相同,我們就需要改寫(xiě) hashcode 方法专肪,比方把 hashcode 值的計(jì)算與 User 對(duì)象的 id 關(guān)聯(lián)起來(lái)刹勃,那么只要 user 對(duì)象擁有相同 id,那么他們的 hashcode 也能保持一致了嚎尤,這樣就可以找到在 hashmap 數(shù)組中的位置了荔仁。如果這個(gè)位置上有多個(gè)元素,還需要用 key 的 equals 方法在對(duì)應(yīng)位置的鏈表中找到需要的元素芽死,所以只改寫(xiě)了 hashcode 方法是不夠的乏梁,equals 方法也是需要改寫(xiě)滴~當(dāng)然啦,按正常思維邏輯关贵,equals 方法一般都會(huì)根據(jù)實(shí)際的業(yè)務(wù)內(nèi)容來(lái)定義遇骑,例如根據(jù) user 對(duì)象的 id 來(lái)判斷兩個(gè) user 是否相等。
在改寫(xiě) equals 方法的時(shí)候揖曾,需要滿(mǎn)足以下三點(diǎn):
(1) 自反性:就是說(shuō) a.equals (a) 必須為 true落萎。
(2) 對(duì)稱(chēng)性:就是說(shuō) a.equals (b)=true 的話亥啦,b.equals (a) 也必須為 true。
(3) 傳遞性:就是說(shuō) a.equals (b)=true练链,并且 b.equals (\c)=true 的話翔脱,a.equals (\c) 也必須為 true。
通過(guò)改寫(xiě) key 對(duì)象的 equals 和 hashcode 方法媒鼓,我們可以將任意的業(yè)務(wù)對(duì)象作為 map 的 key (前提是你確實(shí)有這樣的需要)届吁。

總結(jié):
本文主要描述了 HashMap 的結(jié)構(gòu),和 hashmap 中 hash 函數(shù)的實(shí)現(xiàn)绿鸣,以及該實(shí)現(xiàn)的特性疚沐,同時(shí)描述了 hashmap 中 resize 帶來(lái)性能消耗的根本原因,以及將普通的域模型對(duì)象作為 key 的基本要求潮模。尤其是 hash 函數(shù)的實(shí)現(xiàn)亮蛔,可以說(shuō)是整個(gè) HashMap 的精髓所在,只有真正理解了這個(gè) hash 函數(shù)擎厢,才可以說(shuō)對(duì) HashMap 有了一定的理解尔邓。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市锉矢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌齿尽,老刑警劉巖沽损,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異循头,居然都是意外死亡绵估,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)卡骂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)国裳,“玉大人,你說(shuō)我怎么就攤上這事全跨》熳螅” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵浓若,是天一觀的道長(zhǎng)渺杉。 經(jīng)常有香客問(wèn)我,道長(zhǎng)挪钓,這世上最難降的妖魔是什么是越? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮碌上,結(jié)果婚禮上倚评,老公的妹妹穿的比我還像新娘浦徊。我一直安慰自己,他們只是感情好天梧,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布盔性。 她就那樣靜靜地躺著,像睡著了一般腿倚。 火紅的嫁衣襯著肌膚如雪纯出。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,155評(píng)論 1 299
  • 那天敷燎,我揣著相機(jī)與錄音暂筝,去河邊找鬼。 笑死硬贯,一個(gè)胖子當(dāng)著我的面吹牛焕襟,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播饭豹,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼鸵赖,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了拄衰?” 一聲冷哼從身側(cè)響起它褪,我...
    開(kāi)封第一講書(shū)人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎翘悉,沒(méi)想到半個(gè)月后茫打,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡妖混,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年老赤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片制市。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡抬旺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出祥楣,到底是詐尸還是另有隱情开财,我是刑警寧澤,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布误褪,位于F島的核電站床未,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏振坚。R本人自食惡果不足惜薇搁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望渡八。 院中可真熱鬧啃洋,春花似錦传货、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至孵坚,卻和暖如春粮宛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背卖宠。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工巍杈, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人扛伍。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓筷畦,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親刺洒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子鳖宾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353

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

  • 移步數(shù)據(jù)結(jié)構(gòu)--容器匯總(java & Android)由于 HashMap 底層涉及到太多方面,一篇文章總是不能...
    凱玲之戀閱讀 831評(píng)論 0 5
  • HashMap概述 Hash逆航,又稱(chēng)散列鼎文。哈希表是一種以鍵-值(key-value) 存儲(chǔ)數(shù)據(jù)的,和數(shù)組因俐、鏈表漂问、二叉...
    99793933e682閱讀 559評(píng)論 0 6
  • 學(xué)習(xí)資料; 《Java程序性能優(yōu)化》 美團(tuán)點(diǎn)評(píng)技術(shù)團(tuán)隊(duì) Java 8系列之重新認(rèn)識(shí)HashMap 張旭童大佬 面試...
    英勇青銅5閱讀 2,806評(píng)論 3 97
  • 前言 Map 這樣的Key Value在軟件開(kāi)發(fā)中是非常經(jīng)典的結(jié)構(gòu)女揭,常用于在內(nèi)存中存放數(shù)據(jù)。 本篇主要想討論 Co...
    AI喬治閱讀 1,077評(píng)論 0 23
  • 大多數(shù)中國(guó)父母對(duì)子女教育和培養(yǎng)沒(méi)受過(guò)專(zhuān)門(mén)訓(xùn)練栏饮,又特別是在互聯(lián)網(wǎng)時(shí)代家庭教育的新形勢(shì)下吧兔,更是束手無(wú)策。因此必須...
    hulianwangclub閱讀 614評(píng)論 0 0