從源碼分析java容器之HashMap

引言

本次分析源碼是JDK8版本,我們還是按照之前的分析流程,先來看看HashMap的結構圖

1、HashMap結構圖

HashMap繼承AbstractMap,實現了Map、Cloneable、Serializable接口,Map 是 Key-Value 對映射的抽象接口,HashMap 是基於哈希表的 Map 接口的實現,支持複製、序列化的,如下圖所示:
在這裏插入圖片描述

2、分析源碼

2.1、構造器
//構造方法1
public HashMap() {
	this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//構造方法2
public HashMap(int initialCapacity) {
	this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//構造方法3
public HashMap(int initialCapacity, float loadFactor) {
	if (initialCapacity < 0)
		throw new IllegalArgumentException("Illegal initial capacity: " +
										   initialCapacity);
	if (initialCapacity > MAXIMUM_CAPACITY)
		initialCapacity = MAXIMUM_CAPACITY;
	if (loadFactor <= 0 || Float.isNaN(loadFactor))
		throw new IllegalArgumentException("Illegal load factor: " +
										   loadFactor);
	this.loadFactor = loadFactor;
	this.threshold = tableSizeFor(initialCapacity);
}
//構造方法4
public HashMap(Map<? extends K, ? extends V> m) {
	this.loadFactor = DEFAULT_LOAD_FACTOR;
	putMapEntries(m, false);
}

上圖是HashMap的四個構造方法,我們重點來看第三個構造方法,因爲裏面有兩個重要的參數,容量(capacity)和負載因子(load factor),這裏解釋下:
capacity:我們都知道HashMap底層是數組+鏈表的數據結構,這裏的capacity可以簡單的理解爲數組的長度,官方解釋爲buckets(桶)
load factor:load factor就是buckets填滿程度的最大比例,當存放的buckets大於capacityload factor時,數組擴大爲當前的2倍。
threshold:發生擴容的閥值,數值爲capacity
load factor
提示】:默認的capacity爲16,默認的load factor是0.75,默認的threshold爲12

2.2、put方法

put函數大致的思路爲:

  • 對key的hashCode()做hash,然後再計算index;
  • 如果沒碰撞直接放到bucket裏;
  • 如果碰撞了,以鏈表的形式存在buckets後;
  • 如果碰撞導致鏈表過長(大於等於TREEIFY_THRESHOLD),就把鏈表轉換成紅黑樹;
  • 如果節點已經存在就替換old value(保證key的唯一性)
  • 如果bucket滿了(超過load factor*current capacity),就要resize。 具體代碼的實現如下:
public V put(K key, V value) {
	// 對key的hashCode()做hash算法
	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;
	// tab爲空則創建
	if ((tab = table) == null || (n = tab.length) == 0)
		n = (tab = resize()).length;
	// 計算index,並對null做處理
	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;
	// 超過capacity * load factor 則進行擴容
	if (++size > threshold)
		resize();
	afterNodeInsertion(evict);
	return null;
}
2.3、get方法

理解了put方法,再看get方法就很容易了,思路如下:

  • 如果bucket裏的第一個節點,直接命中,就返回第一個節點;
  • 如果有衝突,則通過key.equals(k)去查找對應的entry
  • 若爲樹,則在樹中通過key.equals(k)查找,時間複雜度O(logn);
  • 若爲鏈表,則在鏈表中通過key.equals(k)查找,時間複雜度O(n)。 具體代碼的實現如下:
public V get(Object key) {
	Node<K,V> e;
	return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
	Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
	if ((tab = table) != null && (n = tab.length) > 0 &&
		(first = tab[(n - 1) & hash]) != null) {
		// 如果直接命中
		if (first.hash == hash && // always check first node
			((k = first.key) == key || (key != null && key.equals(k))))
			return first;
		// 未命中
		if ((e = first.next) != null) {
			//如果是紅黑樹
			if (first instanceof TreeNode)
				return ((TreeNode<K,V>)first).getTreeNode(hash, key);
			// 如果是鏈表
			do {
				if (e.hash == hash &&
					((k = e.key) == key || (key != null && key.equals(k))))
					return e;
			} while ((e = e.next) != null);
		}
	}
	return null;
}

3、總結

3.1、HashMap的特點

HashMap存儲着Entry(hash, key, value, next)對象,可以接收null的鍵值,是非同步的。

3.2、HashMap的工作原理

通過hash的方法,通過put和get存儲和獲取對象。存儲對象時,我們將K/V傳給put方法時,它調用hashCode計算hash從而得到bucket位置,進一步存儲,HashMap會根據當前bucket的佔用情況自動調整容量(超過Load Facotr則resize爲原來的2倍)。獲取對象時,我們將K傳給get,它調用hashCode計算hash從而得到bucket位置,並進一步調用equals()方法確定鍵值對。如果發生碰撞的時候,Hashmap通過鏈表將產生碰撞衝突的元素組織起來,在Java 8中,如果一個bucket中碰撞衝突的元素超過某個限制(默認是8),則使用紅黑樹來替換鏈表,從而提高速度。

3.3、初始化集合,指定的參數initialCapacity大小

initialCapacity = (需要存儲的元素個數 / 負載因子) + 1,負載因子(即loader factor)默認爲0.75,如果無法確定元素個數,則使用默認值爲16。

結束語

HashMap的擴容是非常耗費性能的,所以能判斷元素個數的,最好指定一個初始容量,關於擴容的方法,我們下一篇單獨拿出來講,因爲JDK8在JDK7的基礎上進行了一點改變。

如果本篇對你有所幫助,請順手點個贊,謝謝!

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