JDK1.8源碼中的HashMap

     這幾天工作空閒下來,把部分HashMap源碼抄寫了一遍,仔細研究了裏面一些關鍵的方法。我將閱讀時碰到的問題和答案寫在了代碼註釋中,感覺這樣比較直觀。如果有想學習HashMap源碼的碼友,可以參考。

public class MyHashMap<K, V> implements MyMap<K, V> {
        /* ---------------- Constants -------------- */
	// 默認初始容量 (桶的數量,table數組的長度) capacity都是2的冪,如何保證,resize()
	private static final int INIT_CAPACITY = 1 << 4; 
	// 默認初始加載因子。爲什麼是0.75?理想狀態下哈希表的每個箱子中,元素的數量遵守泊松分佈,當負載因子爲 0.75 時,泊松公式中λ 約等於 0.5,
	// 計算鏈表中數量爲8的概率幾乎爲0.也就是說用0.75作爲加載因子,每個碰撞位置的鏈表長度超過8個是幾乎不可能的。
	private static final float LOAD_FACTOR = 0.75f; 
	// 最大容量,爲什麼不是2^31-1呢?overflow找到一個回答說,在2^30或者更低的範圍來保證安全性,比2^31-1容易的多,沒有必要追求極限。
	private static final int MAX_CAPACITY = 1 << 30; 
	// 桶中的鏈表中節點數量大於等於8,鏈表可能會轉化爲紅黑樹,還需考察MIN_TREEFY_CAPACITY
	private static final int TREEFY_THRESHOLD = 8; 
	// 當節點小於等於6,樹退化爲鏈表
	private static final int UNTREEFY_THRESHOLD = 6; 
	// 鏈表轉化爲樹前,還要判斷,只有鍵值對(注意:不是桶的數量)大於64纔會轉換。防止哈希表建立初期,多個鍵值對剛好放入一個鏈表,導致不必要的轉化;
	private static final int MIN_TREEFY_CAPACITY = 64; 
	/*-------------------fileds 字段----------------*/
	// KV數量,(鏈表,樹中的總和)
	transient int size; 
	// map結構修改的次數,例如添加新Node。注意如果是替換原有Node的舊值,modCount是不會改變的。
	transient int modCount;
	// hashMap的size大於該值時 將resize擴容,threshold=capacity*loadFactor(該threshold爲設置的閾值裝載因子,與下面的當前實際裝載因子需要注意區分)
	int threshold;
	// 裝載因子 用來衡量 map滿的程度,size/capacity,而不是佔用的桶的數量/capacity
	float loadFactor; 
	transient Node<K, V>[] table;
	transient Set<Map.Entry<K, V>> entrySet;
/*-------------------static utilities 靜態方法----------------*/

	static final int hash(Object key) {
		int h;
		// 爲什麼不直接使用key的hashCode?。一,防止開發者寫的hashcode函數性能不佳,散列不均勻。
                //二,key的hashCode的高位參與運算,當數組容量較小時也能保證hash值的均勻。
		return key == null ? 0 : (h = key.hashCode()) ^ (h >>> 16);
	}

	// 經典之處:該算法爲求一個不小於給定值的最小2^次冪
	static int tableSizeFor(int initialCapacity) {
		int n = initialCapacity - 1;// 防止initialCapacity本身就是2的x次冪,那麼經過以下算法,將返回2的x+1次冪;
		n |= n >>> 1;// >>>爲無符號右移,左邊空缺全部補0。經過這一步驟,找出了二進制狀態,最高位爲1的位,並且最高位和次高位都變爲1,結果中前2位變爲了1(如果最高位所在位數大於等於2)
		n |= n >>> 2;// 經過這一步驟,最高2位和次高2位都變爲1,結果中前4位變爲了1(如果最高位所在位數大於等於4)
		n |= n >>> 4;// 經過這一步驟,最高4位和次高4位都變爲1,結果中前8位變爲了1(如果最高位所在位數大於等於8)
		n |= n >>> 8;// 經過這一步驟,最高8位和次高8位都變爲1,結果中前16位變爲了1(如果最高位所在位數大於等於16)
		n |= n >>> 16;// 經過這一步驟,最高16位和次高16位都變爲1,結果中前32位變爲了1(如果最高位所在位數大於等於32)
		return (n < 0) ? 1 : (n >= MAX_CAPACITY ? MAX_CAPACITY : n + 1);// 二進制全部變爲1後,再進1,即可得到不小於該數的最小2^次冪
	}
/*-------------------public operations----------------*/

	public MyHashMap() {
		super();
		this.loadFactor = LOAD_FACTOR;
	}

	public MyHashMap(int initialCapacity) {
		this(initialCapacity, LOAD_FACTOR);
	}

	public MyHashMap(int initialCapacity, float loadFactor) {
		if (initialCapacity < 0)
			throw new RuntimeException("容量不能爲負");
		if (initialCapacity > MAX_CAPACITY)
			initialCapacity = MAX_CAPACITY;
		this.loadFactor = loadFactor;
		this.threshold = tableSizeFor(initialCapacity);
		// 很奇怪 爲什麼是threhold? this.threshold = tableSizeFor(initialCapacity) *loadFactor;
		//貌似這樣才符合意思。其實構造函數並未對table初始化,初始化是在put函數,再到resize函數中進行的,並且在resize中重新計算了threhold值
	}

	public boolean isEmpty() {
		return size == 0;
	}

	public int size() {
		return size;
	}

	public V put(K key, V value) {
		return putVal(hash(key), key, value, false, true);
	}

putVal():

/*
	 * put()思路:
	 * 1.table是否null,是否需要擴容
	 * 2.根據hash計算數組tab索引i,如果tab[i]==null,直接新建節點插入該槽位,轉到步驟6,否則到步驟3
	 * 3.tab[i]的首個元素的key是否與傳入key相同,相同覆蓋value,轉到步驟6,否則到步驟4
	 * 4.判斷tab[i]是否是treeNode,是,插入樹中,轉到步驟6,否則到步驟5
	 * 5.遍歷tab[i]時,如果鏈表的next爲空,進行鏈表的插入操作,插入後,判斷長度是否大於8,是則轉化爲紅黑樹。如果遍歷時發現key值匹配,
	 * 覆蓋value 6.插入成功後,判斷size是否大於threshold,是否需要擴容
	 */
	private 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;// 注意此處n必須重新賦值,不能只是tab賦值,因爲if條件中,有可能只走到前半部分。
		}
		// 巧妙之處:計算槽位的時候本來應該爲hash%n,但是取模運算效率低下。總槽位n爲2的x次冪時,hash%n可以用hash&(n-1)替代,
		//結果一樣,效率卻可以提升5~8倍。
		if ((p = tab[(i = hash & (n - 1))]) == null) {
			tab[i] = new Node<K, V>(hash, key, value, null);
		} else {
			Node<K, V> e;
			K k;
			// 該條件就是判斷tab[i]的key與傳入的key是否相同。用短路與先判斷hash值,效率比直接判斷key相等更高
			if (p.hash == hash && ((p.key == key || (key != null && key.equals(p.key))))) {
				e = p;// 爲什麼不直接p.value=value;
			} else if (p instanceof TreeNode) {
				e = putTreeVal(this, tab, hash, key, value);// 此方法就先不研究了
			
                        } else {
				for (int binCount = 0;; ++binCount) {
					if ((e = p.next) == null) {
						p.next = new Node<K, V>(hash, key, value, null);//e還是null
						if (binCount >= TREEFY_THRESHOLD - 1) {
							// treefyBin(tab,hash);暫不研究
							break;
						}
					}
					if (e.hash == hash && (((k=e.key) == key || (key != null && key.equals(k))))) {
						break;
					}
					p = e;
				}
			}
			//e不爲空 說明map中有key相同的節點
			if (e != null) {
				V oldValue=e.value;
				e.value=value;
				return oldValue;//如果是覆蓋值的操作,直接return,不走到下面,modCount++了,也即只會統計結構改變了的次數
			}
		}
		++modCount;
		if(++size>threshold)
			resize();
		return null;
	}

resize():

        /*讀resize方法之前先明白,由於n(capacity)都是2的x次冪,擴容後,重新計算的節點的索引位置i要麼還是i,
	 * 要麼是i+oldCap。因爲2n-1(32-1:10101)比n-1(16-1:00101)多一個最高位的1,這個1就是
	 * oldCap的值,那麼重新計算索引hash&(2n-1)的時候,只需要看hash & oldCap爲0或1即可,
	 * 爲0新位置還是i,爲1,則新位置爲i+oldCap  */
	private 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>=MAX_CAPACITY) {
				threshold=Integer.MAX_VALUE;
				return oldTab;
			} else if((newCap=oldCap<<1)<MAX_CAPACITY&&oldCap>=INIT_CAPACITY) {//新的數組和新的閾值都擴到2倍
				newThr = oldThr << 1;
			}
		} else if(oldThr>0) {//分支二:oldCap==0,oldThr>0;爲調用有參數的構造函數時所執行
			newCap=oldThr;//通過tableSizeFor得到的2的n次冪在構造函數中賦值給了threshold,在這裏賦值到了capacity,所以下面要重新計算閾值
		} else {//分支三:oldCap==0,oldThr==0;爲調用無參數的構造函數時所執行
			newCap=INIT_CAPACITY;
			newThr=(int) (newCap*loadFactor);
		}
		if(newThr==0) {//只有分支一中的情況二,擴容之後newThr==0
			float ft=newCap*loadFactor;
			newThr=(newCap<MAX_CAPACITY && ft<(float)MAX_CAPACITY)?(int)ft:Integer.MAX_VALUE;
		}
		threshold=newThr;
                //創建新的數組
		@SuppressWarnings("unchecked")
		Node<K,V>[] newTab=(Node<K,V>[])new Node[newCap];
		table=newTab;
		if(oldTab!=null) {//即oldCap>0;爲分支一擴容情況,把舊數組中的每個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) {//紅黑樹
						//tree..
					
                                        } else {//鏈表
						//分別指向新數組中,e.hash&(newCap-1)等於原位置的尾節點和頭節點
						Node<K,V> loTail=null,loHead=null;
						//分別指向新數組中,e.hash&(newCap-1)等於原位置+oldCap的尾節點和頭節點
						Node<K,V> hiTail=null,hiHead=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;//將尾節點指向新的節點
								//原位置+oldCap
							} 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;
						}
						//將重新佈置到原位置+oldCap位置的鏈表的頭節點放到數組槽位中
						if(hiTail!=null) {
							hiTail.next=null;
							newTab[j+oldCap]=hiHead;
						}
					}
				}
			}
		}
		return newTab;
	}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章