文章目錄
- Q1. 默認初始化大小爲什麼是 16 而不是 8 或者 32 ? 爲什麼不直接寫 16 ,而是寫 1<<<4 ?
- Q2. 默認加載因子爲什麼是 0.75 ?
- Q3. 爲什麼有最小樹形化容量閾值 64?
- Q4. HashMap 的 table 的容量如何確定?loadFactor 是什麼?該容量如何變化?這種變化會帶來什麼問題?
- Q5. 爲什麼會擴容 ? 什麼時候會擴容 ? 怎麼擴容 ?
- Q6. 默認初始化大小爲什麼定義爲2的冪 ?
- Q7. 爲何 hashCode 值進行右移運算/異或運算 ?
- Q8. HashMap 中 put 方法的過程 ?
- Q9. HashMap 和 HashTable 有什麼區別 ?
- Q10. Java 中的另一個線程安全的與 HashMap 極其類似的類是什麼?同樣是線程安全,它與 HashTable 在線程同步上有什麼不同?
- Q11. HashMap & ConcurrentHashMap 的區別 ?
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 是什麼?該容量如何變化?這種變化會帶來什麼問題?
- table 數組大小是由 capacity 這個參數確定的,默認是16,也可以構造時傳入,最大限制是1<<30;
- loadFactor 是裝載因子,主要目的是用來確認table 數組是否需要動態擴展,默認值是0.75,比如table 數組大小爲 16,裝載因子爲 0.75 時,threshold 就是12,當 table 的實際大小超過 12 時,table就需要動態擴容;
- 擴容時,調用 resize 方法,將 table 長度變爲原來的兩倍(注意是 table 長度,而不是 threshold)
- 如果數據很大的情況下,擴展時將會帶來性能的損失,在性能要求很高的地方,這種損失很可能很致命。
Q5. 爲什麼會擴容 ? 什麼時候會擴容 ? 怎麼擴容 ?
why ?
當HashMap中的元素越來越多的時候,碰撞的機率也就越來越高(因爲數組的長度是固定的),所以爲了提高查詢的效率,就要對HashMap的數組進行擴容,數組擴容這個操作也會出現在ArrayList中,所以這是一個通用的操作,很多人對它的性能表示過懷疑,不過想想我們的“均攤”原理,就釋然了,而在hashmap數組擴容之後,最消耗性能的點就出現了:原數組中的數據必須重新計算其在新數組中的位置,並放進去,這就是resize。
when ?
當hashmap中的元素個數超過數組大小 * loadFactor時,就會進行數組擴容,loadFactor的默認值爲0.75,也就是說,默認情況下,數組大小爲16,那麼當hashmap中元素個數超過16 * 0.75=12的時候,就把數組的大小擴展爲2 * 16=32,即擴大一倍,然後重新計算每個元素在數組中的位置,而這是一個非常消耗性能的操作,所以如果我們已經預知hashmap中元素的個數,那麼預設元素的個數能夠有效的提高hashmap的性能。
- 元素達到閾值;
- 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