Java HashMap例項原始碼分析

segmentfault發表於2015-08-08

引言

HashMap在鍵值對儲存中被經常使用,那麼它到底是如何實現鍵值儲存的呢?

一 Entry

Entry是Map介面中的一個內部介面,它是實現鍵值對儲存關鍵。在HashMap中,有Entry的實現類,叫做Entry。Entry類很簡單,裡面包含key,value,由外部引入的hash,還有指向下一個Entry物件的引用,和資料結構中學的連結串列中的note節點很類似。

Entry類的屬性和建構函式:

final K key;
V value;
Entry<K,V> next;
int hash;
/**
 * Creates new entry.
 */
Entry(int h, K k, V v, Entry<K,V> n) {
	value = v;
	next = n;
	key = k;
	hash = h;
}

二 HashMap的初始化

//HashMap構造方法
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;
	threshold = initialCapacity;
	init();
}

這是HashMap的建構函式之一,其他建構函式都引用這個建構函式進行初始化。引數InitialCapacity指的是HashMap中table陣列最初的大小,引數loadFactory指的是HashMap可容納鍵值對與陣列長度的比值(舉個例子:陣列長度預設值為16,loadFactory預設值為0.75,如果HashMap中儲存的鍵值對即Entry多於12,則會進行擴容,擴容後大小為當前陣列長度的2倍)。在建構函式中不會對陣列進行初始化,只有在put等操作方法內會進行判斷是否要初始化或擴容。

三 table陣列

在HashMap中有一個概念叫做threshold(實際可容納量),實際可容納量指的是在HashMap中允許存在最多的Entry的個數,它是由HashMap中內建的陣列table的長度*load factory(負載因子)得來。其作用是保證HashMap的效率。

table陣列是HashMap實現鍵值對儲存的又一關鍵,具體鍵值對是怎麼存的呢?請看下圖

JAVA HashMap原始碼淺析

如圖中的[key,value]就是Entry物件來實現的,而table陣列是用來存放Entry物件的。

//陣列的初始化:
private static int roundUpToPowerOf2(int number) {
	return number >= MAXIMUM_CAPACITY
			? MAXIMUM_CAPACITY
			: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
private void inflateTable(int toSize) {
	// Find a power of 2 >= toSize
	int capacity = roundUpToPowerOf2(toSize);
	threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
	table = new Entry[capacity];
	initHashSeedAsNeeded(capacity);
}

在put等方法中發現陣列未進行初始化時會呼叫InflateTable方法進行初始化,輸入引數為初始設定的InitialCapacity,實際上他會呼叫roundUpToPowerOf2方法返回一個比初始容量大的最小的2的冪數(其中一個原因是在得到Entry所在陣列位置時方便)。

四 put方法

public V put(K key, V value) {
	if (table == EMPTY_TABLE) {
		inflateTable(threshold);
	}
	if (key == null)
		return putForNullKey(value);
	int hash = hash(key);
	int i = indexFor(hash, table.length);
	for (Entry<K,V> e = table[i]; e != null; e = e.next) {
		Object k;
		if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
			V oldValue = e.value;
			e.value = value;
			e.recordAccess(this);
			return oldValue;
		}
	}
	modCount++;
	addEntry(hash, key, value, i);
	return null;
}
private V putForNullKey(V value) {
	for (Entry<K,V> e = table[0]; e != null; e = e.next) {
		if (e.key == null) {
			V oldValue = e.value;
			e.value = value;
			e.recordAccess(this);
			return oldValue;
		}
	}
	modCount++;
	addEntry(0, null, value, 0);
	return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
	if ((size >= threshold) && (null != table[bucketIndex])) {
		resize(2 * table.length);
		hash = (null != key) ? hash(key) : 0;
		bucketIndex = indexFor(hash, table.length);
	}
	createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
	Entry<K,V> e = table[bucketIndex];
	table[bucketIndex] = new Entry<>(hash, key, value, e);
	size++;
}

在put方法中

1. 首先會判斷陣列是否為空,如果為空會對陣列進行初始化。

2. 接下來判斷key是否為null,如果為null就採用第二個方法對鍵值對進行put。

3. 接下來對key進行hash得到一個數值,再對這個數值進行處理(IndexFor方法)得到所在陣列中的位置。

4. 接下來會遍歷所在陣列位置的連結串列,如果key的hash和傳入key的hash相同且(key記憶體地址相等 或 equals方法相等),則意味著會更新在連結串列中的value值,並返回舊的value值。

5. 如果上邊的方法都沒有奏效,則會呼叫第三個方法,建立一個新的Entry物件。

在putForNullKey方法中 ,我們看到它是為了NULL值專門設定的,NULL值的hash始終為0,所以key為NULL的Entry物件肯定在陣列的第0個位置。同樣,如果找到則更新,沒有找到則新增。

呼叫addEntry方法 意味著要往這個陣列連結串列中新增一個Entry,所以會在最開始判斷已經存在的Entry數量是否超過了實際可容納量。如果超過了,則會呼叫resize方法將陣列擴大兩倍,注意在擴大之後會對已經存入的Entry進行重排,原因是當初存入時IndexFor方法與陣列長度有關係。接著會呼叫第四個方法。

createEntry方法 很簡單,就是將原本在陣列中存放的連結串列頭置入到新的Entry之後,將新的Entry放入陣列中。從這裡我們可以看出HashMap不保證順序問題。

get方法和contains方法原理和put方法一致,即先通過對key的hash得到其value值所在的連結串列頭在陣列中的位置,再通過equals方法判斷value是否存在。

五 其他

//hash方法
final int hash(Object k) {
	int h = hashSeed;
	if (0 != h && k instanceof String) {
		return sun.misc.Hashing.stringHash32((String) k);
	}
	h ^= k.hashCode();
	// This function ensures that hashCodes that differ only by
	// constant multiples at each bit position have a bounded
	// number of collisions (approximately 8 at default load factor).
	h ^= (h >>> 20) ^ (h >>> 12);
	return h ^ (h >>> 7) ^ (h >>> 4);
}

hash方法中最終返回值與key的hashCode方法有關。

總結

  1. 最終陣列初始化的容量大小會是大於等於你傳入初始容量的最小2的冪數。
  2. key為null或value為null能存入HashMap的原因是對null值會進行單獨的操作。
  3. 在table陣列中的連結串列中每個Entry的共同點是key的hash(key.hashCode)部分相同。
  4. 注意對key的hashCode和equals方法的重寫當你想讓兩個key對映一個物件,因為判定key相等的條件是(hashCode相等+(記憶體相等 或 equals相等))。
  5. 最早存入的鍵值對會在連結串列的末端。
  6. 當陣列沒有連結串列存在時,HashMap效能最好為O(1)。而最差為O(threshould)。

相關文章