1. 程式人生 > >集合原始碼分析(六)HashMap集合

集合原始碼分析(六)HashMap集合

1、HashMap概述:

底層是雜湊演算法,針對鍵。HashMap允許null鍵和null值,執行緒不安全,效率高。鍵不可以重複儲存,值可以。

雜湊結構:不能保證資料的迭代順序,也不能保證順序的恆久不變。

Map集合(無序、無索引、不可以重複)是雙列集合,一個鍵對應一個值。鍵和值之間有一對一的關係。其中鍵不可以重複,值可以。鍵要重寫hashCode()方法和equals()方法。

1.1、基本分析:

HashMap是基於雜湊表實現的,每一個元素是一個key-value對,其內部通過單鏈表解決衝突問題,容量不足(超過了閥值)時,同樣會自動增長。

HashMap是非執行緒安全的,只是用於單執行緒環境下,多執行緒環境下可以採用concurrent併發包下的concurrentHashMap。

HashMap 實現了Serializable介面,因此它支援序列化,實現了Cloneable介面,能被克隆。

HashMap存資料的過程:

HashMap內部維護了一個儲存資料的Entry陣列,HashMap採用連結串列解決衝突,每一個Entry本質上是一個單向連結串列。當準備新增一個key-value對時,首先通過hash(key)方法計算hash值,然後通過indexFor(hash,length)求該key-value對的儲存位置,計算方法是先用hash&0x7FFFFFFF後,再對length取模,這就保證每一個key-value對都能存入HashMap中,當計算出的位置相同時,由於存入位置是一個連結串列,則把這個key-value對插入連結串列頭。

HashMap中key和value都允許為null。key為null的鍵值對永遠都放在以table[0]為頭結點的連結串列中。

1.2、什麼是雜湊表:

在討論雜湊表之前,我們先大概瞭解下其他資料結構在新增,查詢等基礎操作執行效能

  陣列:採用一段連續的儲存單元來儲存資料。對於指定下標的查詢,時間複雜度為O(1);通過給定值進行查詢,需要遍歷陣列,逐一比對給定關鍵字和陣列元素,時間複雜度為O(n),當然,對於有序陣列,則可採用二分查詢,插值查詢,斐波那契查詢等方式,可將查詢複雜度提高為O(logn);對於一般的插入刪除操作,涉及到陣列元素的移動,其平均複雜度也為O(n)

  線性連結串列

:對於連結串列的新增,刪除等操作(在找到指定操作位置後),僅需處理結點間的引用即可,時間複雜度為O(1),而查詢操作需要遍歷連結串列逐一進行比對,複雜度為O(n)

  二叉樹:對一棵相對平衡的有序二叉樹,對其進行插入,查詢,刪除等操作,平均複雜度均為O(logn)。

  雜湊表:相比上述幾種資料結構,在雜湊表中進行新增,刪除,查詢等操作,效能十分之高,不考慮雜湊衝突的情況下,僅需一次定位即可完成,時間複雜度為O(1),接下來我們就來看看雜湊表是如何實現達到驚豔的常數階O(1)的。

我們知道,資料結構的物理儲存結構只有兩種:順序儲存結構鏈式儲存結構(像棧,佇列,樹,圖等是從邏輯結構去抽象的,對映到記憶體中,也這兩種物理組織形式),而在上面我們提到過,在陣列中根據下標查詢某個元素,一次定位就可以達到,雜湊表利用了這種特性,雜湊表的主幹就是陣列

比如我們要新增或查詢某個元素,我們通過把當前元素的關鍵字 通過某個函式對映到陣列中的某個位置,通過陣列下標一次定位就可完成操作。

1.3、HashMap工作原理:

HashMap基於hashing原理,我們通過put()和get()方法儲存和獲取物件。當我們將鍵值對傳遞給put()方法時,它呼叫鍵物件的hashCode()方法來計算hashcode,讓後找到bucket位置來儲存值物件。當獲取物件時,通過鍵物件的equals()方法找到正確的鍵值對,然後返回值物件。HashMap使用LinkedList來解決碰撞問題,當發生碰撞了,物件將會儲存在LinkedList的下一個節點中。 HashMap在每個LinkedList節點中儲存鍵值對物件。

  當兩個不同的鍵物件的hashcode相同時會發生什麼? 它們會儲存在同一個bucket位置的LinkedList中。鍵物件的equals()方法用來找到鍵值對。

2、基本使用:

HashMap<String, String> hm = new HashMap<>();
	hm.put("a", "a");
	hm.put("b", "b");
	hm.put("c", "c");
	hm.put("d", "d");

//遍歷一:keySet()獲取所有鍵的集合

Set<String> set = hm.keySet();	//多型 set 即不是HashSet也不是TreeSet。
			//是keySet。keySet是HashMap的一個內部類。也繼承了AbstractSet
			//相當於是HashSet的兄弟類
for (String key : set) {
	String value = hm.get(key);
	System.out.println(key + ":" + value);
}

//遍歷二:獲取所有鍵值對物件的集合

Set<Entry<String, String>> set = hm.entrySet();//多型。set即不是HashSet也不是TreeSet。
			//是entrySet。entrySet是HashMap的一個內部類。也繼承了AbstractSet
			//相當於是HashSet和keySet的兄弟類
for (Entry<String, String> entry : set) {
	String key = entry.getKey();
	String value = entry.getValue();
	System.out.println(key + ":" + value);
}

3、常用方法原始碼解析:

3.1、V put(K key, V value) :以鍵=值的方式存入Map集合

//原始碼解析
public V put(K key, V value) {
	int hash = hash(key);//計算雜湊值
	int i = indexFor(hash, table.length);//通過h & (length-1)計算應存入陣列中哪個索引處。
	for (Entry<K,V> e = table[i]; e != null; e = e.next) {//遍歷
		Object k;
		//先比較雜湊值
		//再比較是否是同一個物件
		//用equals再比較內部屬性值是否相同
		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);//新增。i表示計算出來的應存入的索引
	return null;
}
//-----------------------------------------------
   void addEntry(int hash, K key, V value, int bucketIndex) {
		if ((size >= threshold) && (null != table[bucketIndex])) {//如果陣列長度超過極限
			resize(2 * table.length);//則擴容,每次擴容都是原先的兩倍。
						//resize方法會新建一個新的兩倍容量的陣列
						//把原來的值利用indexFor方法重新計算應存入的索引
					        //這也就是為什麼不保證順序恆久不變的原因之一
						//因為每次擴容每個元素都會重新計算應存入的索引

			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);//把要新增的鍵值對封裝成整體Entry
														  //陣列原來的值被記錄在新新增的值的next
														  //形成雜湊桶結構。
	size++;
}

3.2、V get(Object key):根據鍵獲取值

//原始碼解析
public V get(Object key) {
	if (key == null)	//判斷是否為null
		return getForNullKey();
	Entry<K,V> entry = getEntry(key);//呼叫getEntry
	return null == entry ? null : entry.getValue();
}
//-----------------------------
final Entry<K,V> getEntry(Object key) {
	if (size == 0) {
		return null;
	}
	int hash = (key == null) ? 0 : hash(key);//先計算雜湊值
	for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
				//利用indexFor計算要查的key應該在陣列中哪個索引
				//從這個索引處記錄的數開始遍歷
				//從連結串列(雜湊桶結構中)
				//挨個進行判斷
		Object k;
		if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
			return e;//如果有就返回
	}
	return null;//如果沒有就返回null
}

3.3、int size():返回Map中鍵值對的個數

//原始碼解析
public int size() {
	return size;//返回size成員變數
//此成員變數在每一次put的底層中的createEntry方法中都會++。
//在每一次刪除元素remove的時候都會--。
}

3.4、boolean containsKey(Object key):判斷Map集合中是否包含鍵為key的鍵值對

//原始碼解析
public boolean containsKey(Object key) {
	return getEntry(key) != null;//請參見get原始碼解析中getEntry方法解析
}

3.5、boolean containsValue(Object value):判斷Map集合中是否包含值為value鍵值對

//原始碼解析
public boolean containsValue(Object value) {//利用雙重for迴圈,判斷是否包含值
	Entry[] tab = table;
	for (int i = 0; i < tab.length ; i++)//遍歷陣列
		for (Entry e = tab[i] ; e != null ; e = e.next)//遍歷連結串列
			if (value.equals(e.value))
				return true;
	return false;
}

3.6、boolean isEmpty():判斷Map集合中是否沒有任何鍵值對

//原始碼解析
public boolean isEmpty() {
	return size == 0;//判斷成員變數是否為0.
}			 //size成員變數在每一次新增的時候都++
			 //每一次刪除的時候都--
			 //請參見size()方法。

3.7、void clear():清空Map集合中所有的鍵值對

//原始碼解析
public void clear() {
	modCount++;
	Arrays.fill(table, null);//將陣列置為null
	size = 0;				 //成員變數size置為0
}
//--------------------------------------
public static void fill(Object[] a, Object val) {
	for (int i = 0, len = a.length; i < len; i++)//迴圈陣列a將每一個元素都置為val
		a[i] = val;//clear中呼叫。就是將陣列都置為null
}

3.8、V remove(Object key):根據鍵值刪除Map中鍵值對

//原始碼解析
public V remove(Object key) {
	Entry<K,V> e = removeEntryForKey(key);
	return (e == null ? null : e.value);
}
//---------------------------------------------------------------------
final Entry<K,V> removeEntryForKey(Object key) {
	int hash = (key == null) ? 0 : hash(key);//計算hashcode值
	int i = indexFor(hash, table.length);//利用hashcode計算在陣列中應存入的索引
	Entry<K,V> prev = table[i];
	Entry<K,V> e = prev;
	while (e != null) {//迴圈遍歷連結串列
		Entry<K,V> next = e.next;
		Object k;
		//先匹配hashcode值,再對比是否同一個物件,再呼叫equals方法比較內部屬性值
		if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))) {
			modCount++;
			size--;
			if (prev == e)
				table[i] = next;
			else
				prev.next = next;
			e.recordRemoval(this);
			return e;
		}
		prev = e;
		e = next;
	}
	return e;
}	

4、HashMap和Hashtable的區別?

HashMap是Hashtable的輕量級實現(非執行緒安全的實現),他們都完成了Map介面。主要的區別有:執行緒安全性,同步(synchronization),以及速度。

1.Hashtable繼承自Dictionary類,而HashMap是Java1.2引進的Map interface的一個實現。

2.HashMap允許將null作為一個entry的key或者value,而Hashtable不允許。

3.HashMap是非synchronized,而Hashtable是synchronized,這意味著Hashtable是執行緒安全的,多個執行緒可以共享一個Hashtable;而如果沒有正確的同步的話,多個執行緒是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的擴充套件性更好。(在多個執行緒訪問Hashtable時,不需要自己為它的方法實現同步,而HashMap 就必須為之提供外同步(Collections.synchronizedMap))

4.另一個區別是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以當有其它執行緒改變了HashMap的結構(增加或者移除元素),將會丟擲ConcurrentModificationException,但迭代器本身的remove()方法移除元素則不會丟擲ConcurrentModificationException異常。但這並不是一個一定發生的行為,要看JVM。這條同樣也是Enumeration和Iterator的區別。fail-fast機制如果不理解原理,可以檢視這篇文章:http://www.cnblogs.com/alexlo/archive/2013/03/14/2959233.html

5.由於HashMap非執行緒安全,在只有一個執行緒訪問的情況下,效率要高於HashTable。

6.HashMap把Hashtable的contains方法去掉了,改成containsvalue和containsKey。因為contains方法容易讓人引起誤解。 

7.Hashtable中hash陣列預設大小是11,增加的方式是 old*2+1。HashMap中hash陣列的預設大小是16,而且一定是2的指數。

8..兩者通過hash值雜湊到hash表的演算法不一樣:

        HashTbale是古老的除留餘數法,直接使用hashcode

int hash = key.hashCode();  
int index = (hash & 0x7FFFFFFF) % tab.length; 

而後者是強制容量為2的冪,重新根據hashcode計算hash值,在使用hash 位與 (hash表長度 – 1),也等價取膜,但更加高效,取得的位置更加分散,偶數,奇數保證了都會分散到。前者就不能保證