天天看點

Java1.8 對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;
	// 隻有非第一次擴容才會進來(第一次擴容在第一次put)
	if (oldCap > 0) {
		// oldCap最大為MAXIMUM_CAPACITY(2^30),可檢視帶參構造方法①
		if (oldCap >= MAXIMUM_CAPACITY) {
			 /**
                 * threshold變成MAX_VALUE(2^31-1),随它們碰撞。但是oldCap不改變,
                 * 因為如果oldCap翻倍就為負數了,如果指派為MAX_VALUE,
                 * 參考 Map容量為什麼不能為MAX_VALUE②
                 */
			threshold = Integer.MAX_VALUE;
			return oldTab;
		}
		// 容量翻倍
		else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
				 oldCap >= DEFAULT_INITIAL_CAPACITY)
			/**
             * 為什麼需要判斷oldCap >= DEFAULT_INITIAL_CAPACITY呢?
             * 應該是容量較小時 capacity * loadFactor造成的誤差比較大,
             * 例如初始化容量為2 threshold則為1,如果每次擴容threshold都翻倍,
             * 那負載因子是0.5了。
             * 為什麼隻小于16呢?
             * 我猜測是在每次擴容都計算threshold和用位運算翻倍之間做權衡
             */
			newThr = oldThr << 1; 
	}
	// 帶參初始化會進入這裡,主要是為了重新算threshold
	else if (oldThr > 0) 
		newCap = oldThr;
	// 不帶參初始化會進入這裡
	else {               
		newCap = DEFAULT_INITIAL_CAPACITY;
		newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
	}
	// 重新算threshold
	if (newThr == 0) {
		float ft = (float)newCap * loadFactor;
		newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
				  (int)ft : Integer.MAX_VALUE);
	}
	threshold = newThr;
	// 擴容
	Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
	table = newTab;
	// 複制資料到新table中
	if (oldTab != null) {
		// 周遊Node
		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 { 
					// 之是以定義兩個頭兩個尾對象,是由于連結清單中的元素的下标在擴容後,要麼是原下标+oldCap,要麼不變,下面會證明
					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) {
						// 尾部節點next設定為null,代碼嚴謹
						loTail.next = null;
						newTab[j] = loHead;
					}
					// 新下标對應的連結清單
					if (hiTail != null) {
						hiTail.next = null;
						newTab[j + oldCap] = hiHead;
					}
				}
			}
		}
	}
	return newTab;
}

①帶參構造方法
public HashMap(int initialCapacity, float loadFactor) {
	if (initialCapacity < 0)
		throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
	// 容量最大為MAXIMUM_CAPACITY(2^30)
	if (initialCapacity > MAXIMUM_CAPACITY)
		initialCapacity = MAXIMUM_CAPACITY;
	if (loadFactor <= 0 || Float.isNaN(loadFactor))
		throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
	this.loadFactor = loadFactor;
	// threshold初始化為最接近initialCapacity的2的幂次方,并且大于或等于initialCapacity。但是在第一次put的時候,threshold會變成threshold * loadFactor
	this.threshold = tableSizeFor(initialCapacity);
}

②Map容量為什麼不能為MAX_VALUE
該為題可轉為:為什麼在Java1.8,每次擴容都為2的幂次方呢?
// 計算下标,下面是map的put和get中都用到計算下标的
(n - 1) & hash

當容量為MAX_VALUE(2^31-1)時,轉換成二進制
	hash
&
	0111 1111 1111 1111 1111 1111 1111 1110
-----------------------------------------------
        xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx0
從上面可看出最低位無論hash是任何值時,都為0,也就是下标隻有2^30種可能,有2^30-1個下标沒有被使用
是以當容量為MAX_VALUE(2^31-1)時會造成一半的空間浪費,效率等同于MAXIMUM_CAPACITY(2^30)

③e.hash & oldCap
該步驟是為了計算位置是否需要移動
因為oldTab的元素下标是根據 hash(key) & (oldCap-1) 計算的,如果擴容後,計算下标是 hash(key) & (2*oldCap-1)
換成二進制就比較清晰了
           
Java1.8 對HashMap的resize()的了解
是以,HashMap擴容的時候,不需要像Java1.7那樣重新算hash值,隻要看e.hash對應2*oldCap-1高位那個
bit是1還是0就好了,是0下标沒變,是1索引變成:原下标+oldCap,這也是Java1.8優化的地方之一。
           

參考:Java 8系列之重新認識HashMap