HashMap 的負載因子

在 Java 基礎(chǔ)中购啄,集合類是很關(guān)鍵的一塊知識點襟企,也是日常開發(fā)的時候經(jīng)常會用到的。比如 List狮含、Map 這些在代碼中也是很常見的顽悼。

個人認為,關(guān)于 HashMap 的實現(xiàn)几迄,JDK 的工程師其實是做了很多優(yōu)化的蔚龙,要說所有的 JDK 源碼中,哪個類埋的彩蛋最多映胁,那我想 HashMap 至少可以排前五木羹。

也正是因為如此,很多細節(jié)都容易被忽視解孙,今天我們就來關(guān)注其中一個問題坑填,那就是:

為什么 HashMap 的負載因子設(shè)置成 0.75,而不是 1 也不是 0.5弛姜?****這背后到底有什么考慮脐瑰?

大家千萬不要小看這個問題,因為負載因子是 HashMap 中很重要的一個概念廷臼,也是高端面試的一個巢栽冢考點。

1. 什么是 loadFactor

首先我們來介紹下什么是負載因子(loadFactor)荠商,如果讀者對這部分已經(jīng)有了解寂恬,那么可以直接跨過這一段。

我們知道莱没,第一次創(chuàng)建 HashMap 的時候掠剑,就會指定其容量(如果未明確指定,默認是 16)郊愧,那隨著我們不斷的向 HashMap 中 put 元素的時候朴译,就有可能會超過其容量井佑,那么就需要有一個擴容機制。

所謂擴容眠寿,就是擴大 HashMap 的容量:

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
    createEntry(hash, key, value, bucketIndex);
}

從代碼中我們可以看到躬翁,在向 HashMap 中添加元素過程中,如果 元素個數(shù)(size)超過臨界值(threshold) 的時候盯拱,就會進行自動擴容(resize)盒发,并且,在擴容之后狡逢,還需要對 HashMap 中原有元素進行 rehash宁舰,即將原來桶中的元素重新分配到新的桶中。

在 HashMap 中奢浑,臨界值(threshold) = 負載因子(loadFactor) * 容量(capacity)蛮艰。

loadFactor 是裝載因子,表示 HashMap 滿的程度雀彼,默認值為 0.75f壤蚜,也就是說默認情況下,當 HashMap 中元素個數(shù)達到了容量的 3/4 的時候就會進行自動擴容徊哑。

2. 為什么要擴容

還記得前面我們說過袜刷,HashMap 在擴容到過程中不僅要對其容量進行擴充,還需要進行 rehash莺丑!所以著蟹,這個過程其實是很耗時的,并且 Map 中元素越多越耗時梢莽。

rehash 的過程相當于對其中所有的元素重新做一遍 hash萧豆,重新計算要分配到那個桶中。

那么蟹漓,有沒有人想過一個問題炕横,既然這么麻煩,為啥要擴容葡粒?HashMap 不是一個數(shù)組鏈表嗎份殿?不擴容的話,也是可以無限存儲的呀嗽交。為啥要擴容卿嘲?

這其實和哈希碰撞有關(guān)!夫壁!

哈希碰撞:

我們知道拾枣,HashMap 其實是底層基于哈希函數(shù)實現(xiàn)的,但是哈希函數(shù)都有如下一個基本特性:根據(jù)同一哈希函數(shù)計算出的哈希值如果不同,那么輸入值肯定也不同梅肤。但是司蔬,根據(jù)同一哈希函數(shù)計算出的哈希值如果相同,輸入值不一定相同姨蝴。

兩個不同的輸入值俊啼,根據(jù)同一哈希函數(shù)計算出的哈希值相同的現(xiàn)象叫做碰撞。

衡量一個哈希函數(shù)的好壞的重要指標就是發(fā)生碰撞的概率以及發(fā)生碰撞的解決方案左医。

而為了解決哈希碰撞授帕,有很多辦法,其中比較常見的就是鏈地址法浮梢,這也是 HashMap 采用的方法跛十。

HashMap 將數(shù)組和鏈表組合在一起,發(fā)揮了兩者的優(yōu)勢秕硝,我們可以將其理解為鏈表的數(shù)組芥映。

image

HashMap 基于鏈表的數(shù)組的數(shù)據(jù)結(jié)構(gòu)實現(xiàn)的。

我們在向 HashMap 中 put 元素的時候缝裤,就需要先定位到是數(shù)組中的哪條鏈表屏轰,然后把這個元素掛在這個鏈表的后面颊郎。

當我們從 HashMap 中 get 元素的時候憋飞,也是需要定位到是數(shù)組中的哪條鏈表,然后再逐一遍歷鏈表中的元素姆吭,直到查找到需要的元素為止榛做。

但是,如果一個 HashMap 中沖突太高内狸,那么數(shù)組的鏈表就會退化為鏈表检眯。這時候查詢速度會大大降低。

image

所以昆淡,為了保證 HashMap 的讀取的速度锰瘸,我們需要想辦法盡量保證 HashMap 的沖突不要太高。

擴容避免哈希碰撞

那么如何能有效的避免哈希碰撞呢昂灵?

我們先反向思維一下避凝,你認為什么情況會導(dǎo)致 HashMap 的哈希碰撞比較多?

無外乎兩種情況:

1眨补、容量太小管削。容量小,碰撞的概率就高了撑螺。狼多肉少含思,就會發(fā)生爭搶。

2、hash 算法不夠好含潘。算法不合理饲做,就可能都分到同一個或幾個桶中。分配不均遏弱,也會發(fā)生爭搶艇炎。

所以,解決 HashMap 中的哈希碰撞也是從這兩方面入手腾窝。

這兩點在 HashMap 中都有很好的體現(xiàn)缀踪。兩種方法相結(jié)合,在合適的時候擴大數(shù)組容量虹脯,再通過一個合適的 hash 算法計算元素分配到哪個數(shù)組中驴娃,就可以大大的減少沖突的概率,就能避免查詢效率低下的問題循集。

3. 為什么默認 loadFactor 是 0.75

解釋了這么多唇敞,我們終于回歸到了標題上的問題。

至此咒彤,我們知道了 loadFactor 是 HashMap 中的一個重要概念疆柔,他表示這個 HashMap 最大的滿的程度。

為了避免哈希碰撞镶柱,HashMap 需要在合適的時候進行擴容旷档。那就是當其中的元素個數(shù)達到臨界值的時候,而這個臨界值前面說過和 loadFactor 有關(guān)歇拆,換句話說鞋屈,設(shè)置一個合理的 loadFactor,可以有效的避免哈希沖突故觅。

那么厂庇,到底 loadFactor 設(shè)置成多少算合適呢?

這個值現(xiàn)在在 JDK 的源碼中是 0.75:

/**
 * The load factor used when none specified in constructor.
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

那么输吏,為什么選擇 0.75 呢权旷?背后有什么考慮?為什么不是 1贯溅,不是 0.8拄氯?不是 0.5,而是 0.75 呢盗迟?

在 JDK 的官方文檔中坤邪,有這樣一段描述描述:

As a general rule, the default load factor (.75) offers a good tradeoff 
between time and space costs. Higher values decrease the space overhead 
but increase the lookup cost (reflected in most of the operations of the 
HashMap class, including get and put).

大概意思是:一般來說,默認的負載因子 (0.75) 在時間和空間成本之間提供了很好的權(quán)衡罚缕。更高的值減少了空間開銷艇纺,但增加了查找成本(反映在 HashMap 類的大多數(shù)操作中,包括 get 和 put)。

試想一下黔衡,如果我們把負載因子設(shè)置成 1蚓聘,容量使用默認初始值 16,那么表示一個 HashMap 需要在 "滿了" 之后才會進行擴容盟劫。

那么在 HashMap 中夜牡,最好的情況是這 16 個元素通過 hash 算法之后分別落到了 16 個不同的桶中,否則就必然發(fā)生哈希碰撞侣签。而且隨著元素越多塘装,哈希碰撞的概率越大,查找速度也會越低影所。

4. 0.75 的數(shù)學(xué)依據(jù)和必然性

理論上我們認為負載因子不能太大蹦肴,不然會導(dǎo)致大量的哈希沖突,也不能太小猴娩,那樣會浪費空間阴幌。

通過一個數(shù)學(xué)推理 log(2)/log(s/(s - 1)) ,當 s 趨于無窮大時卷中,如果增加的鍵的數(shù)量使 P(0) = 0.5矛双,那么 n/s 很快趨近于 log(2),測算出這個數(shù)值在 log(2)(即:0.7 左右)時是比較合理的蟆豫。

當然议忽,這個數(shù)學(xué)計算方法,并不是在 Java 的官方文檔中體現(xiàn)的无埃,而是查詢了一些資料習(xí)得徙瓶,所以也無從考證毛雇,只做參考了嫉称。

那么,為什么最終選定了 0.75 呢灵疮?

還記得前面我們提到過一個公式嗎织阅,就是:臨界值(threshold) = 負載因子(loadFactor) * 容量(capacity)

根據(jù) HashMap 的擴容機制震捣,它會保證 capacity 的值永遠都是 2 的冪荔棉;

那么,為了保證負載因子(loadFactor) * 容量(capacity)的結(jié)果是一個整數(shù)蒿赢,這個值是 0.75(3/4) 比較合理润樱,因為這個數(shù)和任何 2 的冪乘積結(jié)果都是整數(shù)。

5. 總結(jié)(非常重要)

HashMap 是一種 K-V 結(jié)構(gòu)羡棵,為了提升其查詢及插入的速度壹若,底層采用了鏈表的數(shù)組這種數(shù)據(jù)結(jié)構(gòu)實現(xiàn)的。

但是因為在計算元素所在的位置的時候,需要使用 hash 算法店展,而 HashMap 采用的 hash 算法就是鏈地址法养篓。這種方法有兩個極端。

如果 HashMap 中哈希沖突概率高赂蕴,那么 HashMap 就會退化成鏈表(不是真的退化柳弄,而是操作上像是直接操作鏈表),而我們知道概说,鏈表最大的缺點就是查詢速度比較慢碧注,他需要從表頭開始逐一遍歷。

所以糖赔,為了避免 HashMap 發(fā)生大量的哈希沖突应闯,所以需要在適當?shù)臅r候?qū)ζ溥M行擴容。

而擴容的條件是元素個數(shù)達到臨界值時挂捻。

HashMap 中臨界值的計算方法:

臨界值(threshold) = 負載因子(loadFactor) * 容量(capacity)

其中負載因子表示一個數(shù)組可以達到的最大的滿的程度碉纺。這個值不宜太大,也不宜太小刻撒。

loadFactor 太大骨田,比如等于 1,那么就會有很高的哈希沖突的概率声怔,會大大降低查詢速度态贤。

loadFactor 太小,比如等于 0.5醋火,那么頻繁擴容沒悠汽,就會大大浪費空間。

所以芥驳,這個值需要介于 0.5 和 1 之間柿冲。根據(jù)數(shù)學(xué)公式推算。這個值在 log(2) 的時候比較合理兆旬。

另外假抄,為了提升擴容效率,HashMap 的容量(capacity)有一個固定的要求丽猬,那就是一定是 2 的冪宿饱。

所以,如果 loadFactor 是 3/4 的話脚祟,那么和 capacity 的乘積結(jié)果就可以是一個整數(shù)谬以。

最后,一般情況下由桌,我們不建議修改 loadFactor 的值为黎,除非特殊原因胡陪。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市碍舍,隨后出現(xiàn)的幾起案子柠座,更是在濱河造成了極大的恐慌,老刑警劉巖片橡,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件妈经,死亡現(xiàn)場離奇詭異,居然都是意外死亡捧书,警方通過查閱死者的電腦和手機吹泡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來经瓷,“玉大人爆哑,你說我怎么就攤上這事∮咚保” “怎么了揭朝?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長色冀。 經(jīng)常有香客問我潭袱,道長,這世上最難降的妖魔是什么锋恬? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任屯换,我火速辦了婚禮,結(jié)果婚禮上与学,老公的妹妹穿的比我還像新娘彤悔。我一直安慰自己,他們只是感情好索守,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布晕窑。 她就那樣靜靜地躺著,像睡著了一般蕾盯。 火紅的嫁衣襯著肌膚如雪幕屹。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天级遭,我揣著相機與錄音,去河邊找鬼渺尘。 笑死挫鸽,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的鸥跟。 我是一名探鬼主播丢郊,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼盔沫,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了枫匾?” 一聲冷哼從身側(cè)響起架诞,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎干茉,沒想到半個月后谴忧,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡角虫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年沾谓,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片戳鹅。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡均驶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出枫虏,到底是詐尸還是另有隱情妇穴,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布隶债,位于F島的核電站伟骨,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏燃异。R本人自食惡果不足惜携狭,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望回俐。 院中可真熱鬧逛腿,春花似錦、人聲如沸仅颇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽忘瓦。三九已至搁廓,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間耕皮,已是汗流浹背境蜕。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留凌停,地道東北人粱年。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像罚拟,于是被迫代替她去往敵國和親台诗。 傳聞我的和親對象是個殘疾皇子完箩,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348

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