HashMap的擴容過程(jdk1.8版本)
HashMap的常見參數
initialCapacity 默認初始容量 值為16,最大容量值為2^30
loadFactor 默認加載因子 值為0.75f
threshold 閾值 默認值為16 *0.75 ,即容量*加載因子
這兩個參數是影響HashMap性能的重要參數,其中容量表示哈希表中桶的數量,初始容量是創建哈希表時的容量,
加載因子是哈希表在其容量自動增加之前可以達到多滿的一種尺度,它衡量的是一個散列表的空間的使用程度,加載因子越大表示散列表的裝填程度越高,反之愈小。
如果加載因子越大,對空間的利用更充分,然而后果是查找效率的降低;如果加載因子太小,那么散列表的數據將過於稀疏,對空間造成嚴重浪費。系統默認負載因子為0.75,一般情況下無需修改。
在jdk1.7中,hashmap的底層創建的是Entry[]數組,在實例化后,底層就創建了一個長度為16的Entry[]數組,此時的底層結構是數組+鏈表;在jdk1.8中,底層創建的是Node[]數組,底層在一開始並不會創建數組,在第一次調用put方法時,底層才會創建一個長度為16的Node[]數組,此時的底層結構是數組+鏈表+紅黑樹。
何時進行擴容?
HashMap使用的是懶加載,構造完HashMap對象后,只要不進行put 方法插入元素,HashMap並不會去初始化或者擴容table。
當首次調用put方法時,HashMap會發現table為空然后調用resize方法進行初始化。
put方法源碼如下
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
當添加完元素后,如果HashMap發現size(元素總數)大於threshold(閾值),則會調用resize方法進行擴容,然后把擴容后的數組放到新的數組中去。
若threshold(閾值)不為空,table的首次初始化大小為閾值,否則初始化為缺省值大小16。
當table需要擴容時,擴容后的table大小變為原來的兩倍,接下來就是進行擴容后table的調整:
假設擴容前的table大小為2的N次方,有put方法可知,元素的table索引為其hash值的后N位確定
那么擴容后的table大小即為2的N+1次方,則其中元素的table索引為其hash值的后N+1位確定,比原來多了一位
因此,table中的元素只有兩種情況:
- 元素hash值第N+1位為0:不需要進行位置調整
- 元素hash值第N+1位為1:調整至原索引的兩倍位置
在resize方法中,第45行的判斷即用於確定元素hashi值第N+1位是否為0:
- 若為0,則使用loHead與loTail,將元素移至新table的原索引處
- 若不為0,則使用hiHead與hiHead,將元素移至新table的兩倍索引處
擴容或初始化完成后,resize方法返回新的table。
hashmap的resize方法源碼
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}