問:簡單說說你對 HashMap 構造方法中 initialCapacity(初始容量)、loadFactor(加載因子)的理解?
答:這兩個參數(shù)對于 HashMap 來說很重要,直接從一定程度決定了 HashMap 的性能問題。
initialCapacity 初始容量代表了哈希表中桶的初始數(shù)量,即 Entry< K,V>[] table 數(shù)組的初始長度,不過特別注意,table 數(shù)組的長度雖然依賴 initialCapacity,但是每次都會通過 roundUpToPowerOf2(initialCapacity) 方法來保證為 2 的冪次。
loadFactor 加載因子是哈希表在其容量自動增加之前可以達到多滿的一種飽和度百分比,其衡量了一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小。散列當前飽和度的計算為當前 HashMap 中 Entry 的存儲個數(shù)除以當前 table 數(shù)組桶長度,因此當哈希表中 Entry 的數(shù)量超過了 loadFactor 加載因子乘以當前 table 數(shù)組桶長度時就會觸發(fā)擴容操作。對于使用鏈表法的散列表來說,查找一個元素的平均時間是O(1+a),因此如果負載因子越大則對空間的利用更充分,從而導致查找效率的降低,如果負載因子太小則散列表的數(shù)據(jù)將過于稀疏,從而對空間造成浪費。系統(tǒng)默認負載因子為 0.75,一般情況下無需修改。
因此如果哈希桶數(shù)組很大則較差的 Hash 算法分部也會比較分散,如果哈希桶數(shù)組很小則即使好的 Hash 算法也會出現(xiàn)較多碰撞,所以就需要權衡好的 Hash 算法和擴容機制,也就是上面兩個參數(shù)的作用。
問:簡單說說 JDK1.7 中 HashMap 什么情況下會發(fā)生擴容?如何擴容?
答:HashMap 中默認的負載因子為 0.75,默認情況下第一次擴容閥值是 12(16 * 0.75),故第一次存儲第 13 個鍵值對時就會觸發(fā)擴容機制變?yōu)樵瓉頂?shù)組長度的二倍,以后每次擴容都類似計算機制;這也就是為什么 HashMap 在數(shù)組大小不變的情況下存放鍵值對越多查找的效率就會變低(因為 hash 碰撞會使數(shù)組變鏈表),而通過擴容就可以一定程度的平衡查找效率(盡量散列數(shù)組化)的原因所在。
具體的擴容方式對于 JDK 1.8 前后的實現(xiàn)是有一點區(qū)別的,不過大體思路不變。下面給出 JDK 1.7 的具體擴容流程:
//JDK1 .7 擴容最核心的方法,newTable為新容量數(shù)組大小
void transfer (HashMapEntry[]newTable ){
//新容量數(shù)組桶大小為舊的table的2倍
int newCapacity = newTable.length;
// 遍歷舊的數(shù)組桶table
for (HashMapEntry<K, V> e : table) {
// 如果這個數(shù)組位置上有元素且存在哈希沖突的鏈表結構則繼續(xù)遍歷鏈表
while (null != e) {
//取當前數(shù)組索引位上單向鏈表的下一個元素
HashMapEntry<K, V> next = e.next;
//重新依據(jù)hash值計算元素在擴容后數(shù)組中的索引位置
int i = indexFor(e.hash, newCapacity);
//將數(shù)組i的元素賦值給當前鏈表元素的下一個節(jié)點
e.next = newTable[i];
//將鏈表元素放入數(shù)組位置
newTable[i] = e;
//將當前數(shù)組索引位上單向鏈表的下一個元素賦值給e進行新的一圈鏈表遍歷
e = next;
}
}
}
可以看到,整個擴容過程就是一個取出數(shù)組元素(實際數(shù)組索引位置上的每個元素是每個獨立單向鏈表的頭部,也就是發(fā)生 Hash 沖突后最后放入的沖突元素)然后遍歷以該元素為頭的單向鏈表元素,依據(jù)每個被遍歷元素的 hash 值計算其在新數(shù)組中的下標然后進行交換(即原來 hash 沖突的單向鏈表尾部變成了擴容后單向鏈表的頭部)。下面給出圖解流程:
