HashMap 源碼面試相關

Q1. 默認初始化大小爲什麼是 16 而不是 8 或者 32 ? 爲什麼不直接寫 16 ,而是寫 1<<<4 ?

/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

如果太小,4或者8,擴容比較頻繁;如果太大,32或者64甚至太大,又佔用內存空間

位運算更快,不需十進制和二進制相互轉換

Q2. 默認加載因子爲什麼是 0.75 ?

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

加載因子表示哈希表的填滿程度,跟擴容息息相關。爲什麼不是0.5或者1呢?

如果是0.5,就是說哈希表填到一半就開始擴容了,這樣會導致擴容頻繁,並且空間利用率比較低。 如果是1,就是說哈希表完全填滿纔開始擴容,這樣雖然空間利用提高了,但是哈希衝突機會卻大了。

作爲一般規則,默認負載因子(0.75)在時間和空間成本上提供了良好的權衡。負載因子數值越大,空間開銷越低,但是會提高查找成本(體現在大多數的HashMap類的操作,包括get和put)。設置初始大小時,應該考慮預計的entry數在map及其負載係數,並且儘量減少rehash操作的次數。如果初始容量大於最大條目數除以負載因子,rehash操作將不會發生。

簡言之, 負載因子0.75就是衝突的機會 與空間利用率權衡的最後體現,也是一個程序員實驗的經驗值。

Q3. 爲什麼有最小樹形化容量閾值 64?

/**
 * The smallest table capacity for which bins may be treeified.
 * (Otherwise the table is resized if too many nodes in a bin.)
 * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
 * between resizing and treeification thresholds.
 */
static final int MIN_TREEIFY_CAPACITY = 64;

如果數組長度小於 64, 這個時候樹形化,治標不治本,因爲引起鏈表過長的根本原因是數組過短。

所以在JDK1.8源碼中,執行樹形化之前,會先檢查數組長度,如果長度小於64,則對數組進行擴容,而不是進行樹形化。

Q4. HashMap 的 table 的容量如何確定?loadFactor 是什麼?該容量如何變化?這種變化會帶來什麼問題?

  1. table 數組大小是由 capacity 這個參數確定的,默認是16,也可以構造時傳入,最大限制是1<<30;
  2. loadFactor 是裝載因子,主要目的是用來確認table 數組是否需要動態擴展,默認值是0.75,比如table 數組大小爲 16,裝載因子爲 0.75 時,threshold 就是12,當 table 的實際大小超過 12 時,table就需要動態擴容;
  3. 擴容時,調用 resize 方法,將 table 長度變爲原來的兩倍(注意是 table 長度,而不是 threshold)
  4. 如果數據很大的情況下,擴展時將會帶來性能的損失,在性能要求很高的地方,這種損失很可能很致命。

Q5. 爲什麼會擴容 ? 什麼時候會擴容 ? 怎麼擴容 ?

why ?

當HashMap中的元素越來越多的時候,碰撞的機率也就越來越高(因爲數組的長度是固定的),所以爲了提高查詢的效率,就要對HashMap的數組進行擴容,數組擴容這個操作也會出現在ArrayList中,所以這是一個通用的操作,很多人對它的性能表示過懷疑,不過想想我們的“均攤”原理,就釋然了,而在hashmap數組擴容之後,最消耗性能的點就出現了:原數組中的數據必須重新計算其在新數組中的位置,並放進去,這就是resize。

when ?

當hashmap中的元素個數超過數組大小 * loadFactor時,就會進行數組擴容,loadFactor的默認值爲0.75,也就是說,默認情況下,數組大小爲16,那麼當hashmap中元素個數超過16 * 0.75=12的時候,就把數組的大小擴展爲2 * 16=32,即擴大一倍,然後重新計算每個元素在數組中的位置,而這是一個非常消耗性能的操作,所以如果我們已經預知hashmap中元素的個數,那麼預設元素的個數能夠有效的提高hashmap的性能。

  1. 元素達到閾值;
  2. HashMap 準備樹形化但又發現數組太短。

what ?

創建一個新的數組,其容量爲舊數組的兩倍,並重新計算舊數組中結點的存儲位置。結點在新數組中的位置只有兩種,原下標位置或原下標+舊數組的大小。

Q6. 默認初始化大小爲什麼定義爲2的冪 ?

數組下標索引的定位公式是:i = (n - 1)&hash

if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);

(HashMap 中的 hash 算法)

核心概念: HashMap 是根據 Key 的 hash 值和數組的長度取模得到一個值,從而定位到桶的位置。
取模可以改爲 hashCode & (length - 1)

當初始化大小 n 是2的倍數時, (n - 1)&hash 等價於 n%hash:n - 1意味着比 n 最高位小的位都爲1,而高的位都爲0,因此通過與可以剔除位數比 n 最高位更高的部分,只保留比n最高位小的部分,也就是取餘了。

HashMap 底層:數組+鏈表+紅黑樹

定位數組下標用的是與運算&,爲什麼不用取餘呢?

位運算直接對內存數據進行操作,不需要轉成十進制,因此位運算要比取模運算的效率更高,所以 HashMap 在計算元素要存放在數組中的 index 的時候,使用位運算代替了取模運算。之所以可以做等價代替,前提是要求 HashMap 的容量一定要是 2^n 。

因此,默認初始化大定義爲2的冪,就是爲了使用更高效的與運算。

Q7. 爲何 hashCode 值進行右移運算/異或運算 ?

JDK8 中的 hash 算法:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

首先是取 key 的 hashCode 算法,然後把它右移16位,然後取異或

int是4個字節,也就是32位,我們右移16位也即是把高位的數據右移到低位的16位,然後做異或,那就是把高位和低位的數據進行重合,同時保留了低位和高位的信息

舉個例子:

首先,假設有一種情況,對象 A 的 hashCode 爲 1000010001110001000001111000000,對象 B 的 hashCode 爲 0111011100111000101000010100000。

如果數組長度是16,也就是 15 與運算這兩個數(前面說的hashCode & (length - 1)), 你會發現結果都是0。這樣的散列結果太讓人失望了。很明顯不是一個好的散列算法。

但是如果我們將 hashCode 值右移 16 位,也就是取 int 類型的一半,剛好將該二進制數對半切開。並且使用位異或運算(如果兩個數對應的位置相反,則結果爲1,反之爲0),這樣的話,就能避免我們上面的情況的發生。簡而言之就是儘量打亂hashCode的低16位,因爲真正參與運算的還是低16位。

不知道這種解釋是否是簡單明瞭,經過自己的思考和分析後 也明白了 這段代碼設計的初衷,也會感嘆設計者的精妙。

Q8. HashMap 中 put 方法的過程 ?

“調用哈希函數獲取Key對應的hash值,再計算其數組下標;

如果沒有出現哈希衝突,則直接放入數組;如果出現哈希衝突,則以鏈表的方式放在鏈表後面;

如果鏈表長度超過閥值( TREEIFY THRESHOLD==8),就把鏈表轉成紅黑樹,鏈表長度低於6,就把紅黑樹轉回鏈表;

如果結點的key已經存在,則替換其value即可;

如果集合中的鍵值對大於12,調用resize方法進行數組擴容。”

Q9. HashMap 和 HashTable 有什麼區別 ?

  • HashMap 是線程不安全的,HashTable 是線程安全的;
  • 由於線程安全,所以 HashTable 的效率比不上 HashMap;
  • HashMap最多隻允許一條記錄的鍵爲null,允許多條記錄的值爲null,而 HashTable不允許;
  • HashMap 默認初始化數組的大小爲16,HashTable 爲 11,前者擴容時,擴大兩倍,後者擴大兩倍+1;
  • HashMap 需要重新計算 hash 值,而 HashTable 直接使用對象的 hashCode

Q10. Java 中的另一個線程安全的與 HashMap 極其類似的類是什麼?同樣是線程安全,它與 HashTable 在線程同步上有什麼不同?

ConcurrentHashMap 類(是 Java併發包 java.util.concurrent 中提供的一個線程安全且高效的 HashMap 實現)。

HashTable 是使用 synchronize 關鍵字加鎖的原理(就是對對象加鎖);

而針對 ConcurrentHashMap,在 JDK 1.7 中採用 分段鎖的方式;JDK 1.8 中直接採用了CAS(無鎖算法)+ synchronized。

Q11. HashMap & ConcurrentHashMap 的區別 ?

除了加鎖,原理上無太大區別。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    ......
}

HashMap最多隻允許一條記錄的鍵爲null,允許多條記錄的值爲null,而 ConcurrentHashMap 不允許;

因爲hashtable,concurrenthashmap它們是用於多線程的,併發的 ,如果map.get(key)得到了null,不能判斷到底是映射的value是null,還是因爲沒有找到對應的key而爲空,而用於單線程狀態的hashmap卻可以用containKey(key) 去判斷到底是否包含了這個null。

hashtable爲什麼就不能containKey(key)

一個線程先get(key)再containKey(key),這兩個方法的中間時刻,其他線程怎麼操作這個key都會可能發生,例如刪掉這個key

說明:網上搜集整理後作了部分修改而來,小部分散裝內容原文出處找不到鏈接了。

參考鏈接: https://baijiahao.baidu.com/s?id=1664890257742088649&wfr=spider&for=pc

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章