1. 程式人生 > >Java 1.8中HashMap的resize()方法擴容部分的理解

Java 1.8中HashMap的resize()方法擴容部分的理解

首先可以看這篇文章,對擴容前面部分進行了解:Java 8系列之重新認識HashMap
###沒有別的說的,程式設計師直接看程式碼。
紅黑樹比較麻煩,直接刪除了。

// 擴容兼初始化
	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) {
				// 原陣列長度大於最大容量(1073741824) 則將threshold設為Integer.MAX_VALUE=2147483647
				// 接近MAXIMUM_CAPACITY的兩倍
				threshold = Integer.MAX_VALUE;
				return oldTab;
			} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {
				// 新陣列長度 是原來的2倍,
				// 臨界值也擴大為原來2倍
				newThr = oldThr << 1;
			}
		} else if (oldThr > 0) {
			// 如果原來的thredshold大於0則將容量設為原來的thredshold
			// 在第一次帶引數初始化時候會有這種情況
			newCap = oldThr;
		} else {
			// 在預設無引數初始化會有這種情況
			newCap = DEFAULT_INITIAL_CAPACITY;// 16
			newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);// 0.75*16=12
		}
		if (newThr == 0) {
			// 如果新 的容量 ==0
			float ft = (float) newCap * loadFactor;// loadFactor 雜湊載入因子 預設0.75,可在初始化時傳入,16*0.75=12 可以放12個鍵值對
			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;
		// 如果原來的table有資料,則將資料複製到新的table中
		if (oldTab != null) {
			// 根據容量進行迴圈整個陣列,將非空元素進行復制
			for (int j = 0; j < oldCap; ++j) {
				Node<K, V> e;
				// 獲取陣列的第j個元素
				if ((e = oldTab[j]) != null) {
					oldTab[j] = null;
					// 如果連結串列只有一個,則進行直接賦值
					if (e.next == null)
						// e.hash & (newCap - 1) 確定元素存放位置
						newTab[e.hash & (newCap - 1)] = e;
					//  此處省略紅黑樹
					else {
						// 進行連結串列複製
						// 方法比較特殊: 它並沒有重新計算元素在陣列中的位置
						// 而是採用了 原始位置加原陣列長度的方法計算得到位置
						Node<K, V> loHead = null, loTail = null;
						Node<K, V> hiHead = null, hiTail = null;
						Node<K, V> next;
						do {
							/*********************************************/
							/**
							 * 注: e本身就是一個連結串列的節點,它有 自身的值和next(連結串列的值),但是因為next值對節點擴容沒有幫助,
							 * 所有在下面討論中,我近似認為 e是一個只有自身值,而沒有next值的元素。
							 */
							/*********************************************/
							next = e.next;
							// 注意:不是(e.hash & (oldCap-1));而是(e.hash & oldCap)

							// (e.hash & oldCap) 得到的是 元素的在陣列中的位置是否需要移動,示例如下
							// 示例1:
							// e.hash=10 0000 1010
							// oldCap=16 0001 0000
							//	 &   =0	 0000 0000       比較高位的第一位 0
							//結論:元素位置在擴容後陣列中的位置沒有發生改變
							
							// 示例2:
							// e.hash=17 0001 0001
							// oldCap=16 0001 0000
							//	 &   =1	 0001 0000      比較高位的第一位   1
							//結論:元素位置在擴容後陣列中的位置發生了改變,新的下標位置是原下標位置+原陣列長度
							
							// (e.hash & (oldCap-1)) 得到的是下標位置,示例如下
							//   e.hash=10 0000 1010
							// oldCap-1=15 0000 1111
							//      &  =10 0000 1010
								
							//   e.hash=17 0001 0001
							// oldCap-1=15 0000 1111
							//      &  =1  0000 0001
							
							//新下標位置
							//   e.hash=17 0001 0001
							// newCap-1=31 0001 1111    newCap=32
							//      &  =17 0001 0001    1+oldCap = 1+16
							
							//元素在重新計算hash之後,因為n變為2倍,那麼n-1的mask範圍在高位多1bit(紅色),因此新的index就會發生這樣的變化:
							//參考博文:[Java8的HashMap詳解](https://blog.csdn.net/login_sonata/article/details/76598675)  
							// 0000 0001->0001 0001

							if ((e.hash & oldCap) == 0) {
								// 如果原元素位置沒有發生變化
								if (loTail == null)
									loHead = e;// 確定首元素
								// 第一次進入時	  e   -> aa  ; loHead-> aa
								else
									loTail.next = e;
								//第二次進入時		loTail-> aa  ;    e  -> bb ;  loTail.next -> bb;而loHead和loTail是指向同一塊記憶體的,所以loHead.next 地址為 bb  
								//第三次進入時		loTail-> bb  ;    e  -> cc ;  loTail.next 地址為 cc;loHead.next.next = cc
								loTail = e;
								// 第一次進入時   	  e   -> aa  ; loTail-> aa loTail指向了和  loHead相同的記憶體空間
								// 第二次進入時               e   -> bb  ; loTail-> bb loTail指向了和  loTail.next(loHead.next)相同的記憶體空間   loTail=loTail.next
								// 第三次進入時               e   -> cc  ; loTail-> cc loTail指向了和  loTail.next(loHead.next.next)相同的記憶體
							} else {
								//與上面同理
								
								if (hiTail == null)
									hiHead = e;
								else
									hiTail.next = e;
								hiTail = e;
							}
						} while ((e = next) != null);//這一塊就是 舊連結串列遷移新連結串列
						//總結:1.8中 舊連結串列遷移新連結串列    連結串列元素相對位置沒有變化; 實際是對物件的記憶體地址進行操作 
						//在1.7中  舊連結串列遷移新連結串列        如果在新表的陣列索引位置相同,則連結串列元素會倒置
						if (loTail != null) {
							loTail.next = null;// 將連結串列的尾節點 的next 設定為空
							newTab[j] = loHead;
						}
						if (hiTail != null) {
							hiTail.next = null;// 將連結串列的尾節點 的next 設定為空
							newTab[j + oldCap] = hiHead;
						}
					}
				}
			}
		}
		return newTab;
	}

擴容中在新陣列中存放元素的流程圖解:

注: e本身就是一個連結串列的節點,它有 自身的值和next(連結串列的值),但是因為next值對節點擴容沒有幫助,所有在下面討論中,我近似認為 e是一個只有自身值,而沒有next值的元素。

    在每次擴容前,e的next值有專門的next變數接收(next = e.next)

流程圖
注:圖中aa,bb,cc 代指元素在儲存的地址