我說我瞭解集合類,面試官居然問我爲啥HashMap的負載因子不設置成1!?

在Java基礎中,集合類是很關鍵的一塊知識點,也是平常開發的時候常常會用到的。好比List、Map這些在代碼中也是很常見的。html

我的認爲,關於HashMap的實現,JDK的工程師實際上是作了不少優化的,要說全部的JDK源碼中,哪一個類埋的彩蛋最多,那我想HashMap至少能夠排前五。java

也正是由於如此,不少細節都容易被忽視,今天咱們就來關注其中一個問題,那就是:面試

爲何HashMap的負載因子設置成0.75,而不是1也不是0.5?這背後到底有什麼考慮?算法

你們千萬不要小看這個問題,由於負載因子是HashMap中很重要的一個概念,也是高端面試的一個常考點。api

另外,這個值得設置,有些人會用錯的,好比前幾天個人《阿里巴巴Java開發手冊建議建立HashMap時設置初始化容量,可是多少合適呢?》這篇文章中,就有讀者這樣回覆:數組

-w356

-w375

既然有人會嘗試着去修改負載因子,那麼到底改爲1是否是合適呢?爲何HashMap不使用1做爲負載因子的默認值呢?數據結構

什麼是loadFactor

首先咱們來介紹下什麼是負載因子(loadFactor),若是讀者對這部分已經有了解,那麼能夠直接跨過這一段。oracle

咱們知道,第一次建立HashMap的時候,就會指定其容量(若是未顯示制定,默認是16,詳見爲啥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中添加元素過程當中,若是 元素個數(size)超過臨界值(threshold) 的時候,就會進行自動擴容(resize),而且,在擴容以後,還須要對HashMap中原有元素進行rehash,即將原來通中的元素從新分配到新的桶中。

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

loadFactor是裝載因子,表示HashMap滿的程度,默認值爲0.75f,也就是說默認狀況下,當HashMap中元素個數達到了容量的3/4的時候就會進行自動擴容。(相見HashMap中傻傻分不清楚的那些概念

爲何要擴容

還記得前面咱們說過,HashMap在擴容到過程當中不只要對其容量進行擴充,還須要進行rehash!因此,這個過程實際上是很耗時的,而且Map中元素越多越耗時。

rehash的過程至關於對其中全部的元素從新作一遍hash,從新計算要分配到那個桶中。

那麼,有沒有人想過一個問題,既然這麼麻煩,爲啥要擴容?HashMap不是一個數組鏈表嗎?不擴容的話,也是能夠無限存儲的呀。爲啥要擴容?

這其實和哈希碰撞有關。

哈希碰撞

咱們知道,HashMap實際上是底層基於哈希函數實現的,可是哈希函數都有以下一個基本特性:根據同一哈希函數計算出的散列值若是不一樣,那麼輸入值確定也不一樣。可是,根據同一散列函數計算出的散列值若是相同,輸入值不必定相同。

兩個不一樣的輸入值,根據同一散列函數計算出的散列值相同的現象叫作碰撞。

衡量一個哈希函數的好壞的重要指標就是發生碰撞的機率以及發生碰撞的解決方案。

而爲了解決哈希碰撞,有不少辦法,其中比較常見的就是鏈地址法,這也是HashMap採用的方法。詳見全網把Map中的hash()分析的最透徹的文章,別無二家。

HashMap將數組和鏈表組合在一塊兒,發揮了二者的優點,咱們能夠將其理解爲鏈表的數組。

-w648

HashMap基於鏈表的數組的數據結構實現的

咱們在向HashMap中put元素的時候,就須要先定外到是數組中的哪條鏈表,而後把這個元素掛在這個鏈表的後面。

當咱們從HashMap中get元素的時候,也是須要定位到是數組中的哪條鏈表,而後再逐一遍歷鏈表中的元素,直到查找到須要的元素爲止。

可見,HashMap經過鏈表的數組這種結構,解決了hash衝突的問題。

可是,若是一個HashMap中衝突過高,那麼數組的鏈表就會退化爲鏈表。這時候查詢速度會大大下降。

-w773

因此,爲了保證HashMap的讀取的速度,咱們須要想辦法儘可能保證HashMap的衝突不要過高。

擴容避免哈希碰撞

那麼如何能有效的避免哈希碰撞呢?

咱們先反向思惟一下,你認爲何狀況會致使HashMap的哈希碰撞比較多?

無外乎兩種狀況:

一、容量過小。容量小,碰撞的機率就高了。狼多肉少,就會發生爭強。

二、hash算法不夠好。算法不合理,就可能都分到同一個或幾個桶中。分配不均,也會發生爭強。

因此,解決HashMap中的哈希碰撞也是從這兩方面入手。

這兩點在HashMap中都有很好的提現。兩種方法相結合,在合適的時候擴大數組容量,再經過一個合適的hash算法計算元素分配到哪一個數組中,就能夠大大的減小衝突的機率。就能避免查詢效率低下的問題。

爲何默認loadFactor是0.75

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

爲了不哈希碰撞,HashMap須要在合適的時候進行擴容。那就是當其中的元素個數達到臨界值的時候,而這個臨界值前面說過和loadFactor有關,換句話說,設置一個合理的loadFactor,能夠有效的避免​哈希衝突。

那麼,到底loadFactor設置成多少算合適呢?

這個值如今在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)在時間和空間成本之間提供了很好的權衡。更高的值減小了空間開銷,但增長了查找成本(反映在HashMap類的大多數操做中,包括get和put)。

試想一下,若是咱們把負載因子設置成1,容量使用默認初始值16,那麼表示一個HashMap須要在"滿了"以後纔會進行擴容。

那麼在HashMap中,最好的狀況是這16個元素經過hash算法以後分別落到了16個不一樣的桶中,不然就必然發生哈希碰撞。並且隨着元素越多,哈希碰撞的機率越大,查找速度也會越低。

0.75的數學依據

另外,咱們能夠經過一種數學思惟來計算下這個值是多少合適。​

咱們假設一個bucket空和非空的機率爲0.5,咱們用s表示容量,n表示已添加元素個數。

用s表示添加的鍵的大小和n個鍵的數目。根據二項式定理,桶爲空的機率爲:

P(0) = C(n, 0) * (1/s)^0 * (1 - 1/s)^(n - 0)
複製代碼

所以,若是桶中元素個數小於如下數值,則桶多是空的:

log(2)/log(s/(s - 1))
複製代碼

當s趨於無窮大時,若是增長的鍵的數量使P(0) = 0.5,那麼n/s很快趨近於log(2):

log(2) ~ 0.693...
複製代碼

因此,合理值大概在0.7左右。

固然,這個數學計算方法,並非在Java的官方文檔中提現的,咱們也無從考察到底有沒有這層考慮,就像咱們根本不知道魯迅寫文章時候怎麼想的同樣,只能推測。這個推測來源於Stack Overflor(stackoverflow.com/questions/1…

0.75的必然因素

理論上咱們認爲負載因子不能太大,否則會致使大量的哈希衝突,也不能過小,那樣會浪費空間。

經過一個數學推理,測算出這個數值在0.7左右是比較合理的。

那麼,爲何最終選定了0.75呢?

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

咱們在《爲啥HashMap的默認容量是16?》中介紹過,根據HashMap的擴容機制,他會保證capacity的值永遠都是2的冪。

那麼,爲了保證負載因子(loadFactor) * 容量(capacity)的結果是一個整數,這個值是0.75(3/4)比較合理,由於這個數和任何2的冪乘積結果都是整數。

總結

HashMap是一種K-V結構,爲了提高其查詢及插入的速度,底層採用了鏈表的數組這種數據結構實現的。

可是由於在計算元素所在的位置的時候,須要使用hash算法,而HashMap採用的hash算法就是鏈地址法。這種方法有兩個極端。

若是HashMap中哈希衝突機率高,那麼HashMap就會退化成鏈表(不是真的退化,而是操做上像是直接操做鏈表),而咱們知道,鏈表最大的缺點就是查詢速度比較慢,他須要從表頭開始逐一遍歷。

因此,爲了不HashMap發生大量的哈希衝突,因此須要在適當的時候對其進行擴容。

而擴容的條件是元素個數達到臨界值時。HashMap中臨界值的計算方法:

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

其中負載因子表示一個數組能夠達到的最大的滿的程度。這個值不宜太大,也不宜過小。

loadFactor太大,好比等於1,那麼就會有很高的哈希衝突的機率,會大大下降查詢速度。

loadFactor過小,好比等於0.5,那麼頻繁擴容沒,就會大大浪費空間。

因此,這個值須要介於0.5和1之間。根據數學公式推算。這個值在log(2)的時候比較合理。

另外,爲了提高擴容效率,HashMap的容量(capacity)有一個固定的要求,那就是必定是2的冪。

因此,若是loadFactor是3/4的話,那麼和capacity的乘積結果就能夠是一個整數。

因此,通常狀況下,咱們不建議修改loadFactor的值,除非特殊緣由。

好比我明確的知道個人Map只存5個kv,而且永遠不會改變,那麼能夠考慮指定loadFactor。

可是其實我也不建議這樣用。咱們徹底能夠經過指定capacity達到這樣的目的。詳見爲啥HashMap的默認容量是16?

參考資料:

stackoverflow.com/questions/1…

docs.oracle.com/javase/6/do…

preshing.com/20110504/ha…

相關文章
相關標籤/搜索