天天看點

JAVA集合Map集合詳解之HashMap/hashTable

Map是映射鍵值的對象。map不能包含重複鍵:每個鍵最多隻能映射一個值。它模拟了數學函數的抽象。Map接口包括基本操作的方法(如put、get、remove、containsKey、containsValue、size和empty)、批量操作(如putAll和clear)和集合視圖(如keySet、entrySet和values)。Java平台包含三個通用的映射實作:HashMap、TreeMap和LinkedHashMap。它們的行為和性能與Set接口部分中描述的HashSet、TreeSet和LinkedHashSet類似。

下面從HashMap和hashTable兩個容器分别介紹對比介紹一下。

下面我來分别看一下HashMap 和 hashTable 在無參構造函數執行個體化的具體執行個體:

JAVA集合Map集合詳解之HashMap/hashTable
JAVA集合Map集合詳解之HashMap/hashTable

由上圖我們可以看到HashMap的無參數構造函數new 了一個:容量為16,加載因子為0.75,門檻值為12的容器。

而hashTable的無參數構造函數則new 了一個:容量為11,加載因子為0.75,門檻值為8的容器。

其中門檻值為容量和加載因子的乘積,意思是如果容器到了這個值,那麼就要實施擴容的機制了。下面我們看一下這兩個容器到了門檻值分别是如何擴容的呢?

首先是HashMap的擴容機制:

JAVA集合Map集合詳解之HashMap/hashTable

從源碼上看,容器擴大了原容器的length*2倍。必須是2的冥。(2的幾次方)

JAVA集合Map集合詳解之HashMap/hashTable

裡面還有一個判斷如果原來容器的容量已經達到了最大值,那麼就把門檻值調整到最大值,然後把原數組資料映射到新的更大的數組當中。這也就是說當資料量過多并且知道最大值的時候為了避免哈希表被重新散列(防止内部資料結構頻繁被重新建構)。

然後我們看一下hashTable是如何擴容的:

JAVA集合Map集合詳解之HashMap/hashTable

原容器的大小乘以2+1,保證得到的資料是一個奇數。那麼到這了我們考慮一下為什麼table擴容要求是奇數,而map擴容必須是2的冥呢?

那麼我們下面說一下HashMap的擴容機制以及确認元素位置的源碼,來分析一下為什麼設計成2的冥:

通過位運算符保證初始容量一定是2的冥

JAVA集合Map集合詳解之HashMap/hashTable

為了防止hash碰撞,在Entry數組(單連結清單)中為了保證每一個位置隻有一個元素,通過hash%table.length=bucketIndex,bucketIndex為元素具體的位置,這樣能夠均勻的分布到容器的各個位置且不會有重複的。對集合操作效率也高。那麼我們看一下源碼中是如何找到元素具體的位置的:

JAVA集合Map集合詳解之HashMap/hashTable

為了減少碰撞HashMap是做了二次hash運算的。

JAVA集合Map集合詳解之HashMap/hashTable

h為最後計算的hash值,length為容器的容量。假設容量 = 16,我們計算一個hash值,來看一下h具體值為,并且我們計算一下indexFor的值是什麼:

JAVA集合Map集合詳解之HashMap/hashTable

可以看到具體的hash值和在容器中的一個位置資訊。

JAVA集合Map集合詳解之HashMap/hashTable

然後我們看一下,巧合的是根據我們的計算h & (length - 1) == h % length兩個等式正好相等。且

JAVA集合Map集合詳解之HashMap/hashTable

這個的位運算的效率更高,這個應該就是容量必須為2的幂的原因。保證了資料分散的均勻。

并且通過二次hash減少碰撞,那麼什麼是碰撞呢?碰撞就是兩個數計算出來的hash值一樣,且equals e1.equals(e2)不相等,這樣在一個hash位置上就會存儲多個連結清單。在取值或者删除資料元素的時候效率比較低。

上面就是說的HashMap的容量為什麼是2的冥的原因,下面來介紹一下hashTable的初始容量為什麼是11,以及擴容機制?

JAVA集合Map集合詳解之HashMap/hashTable

hashTable的key擷取hash為直接傳回的目前key的hashCode值例如:如果是一個String的lisi傳回3322014。

通過拆分lisi為char數組元素,且每個值拿到ASCII值的十進制。31*hash + 目前碼值。

直接計算目前hash & long int的最大值%目前容器的容量,獲得具體在容器中的位置。

JAVA集合Map集合詳解之HashMap/hashTable

int newCapacity = (oldCapacity << 1) + 1;這個是hashTable的一個擴容計算規則:保證了擴容後容量始終為奇數。

那麼hashTable的擴容容量始終保證為奇數呢?

首先我猜測跟他的确認位址是有關系的,在就是由于hashTable全程加了同步鎖為線程安全的,為了性能更高的操作容器才會這麼設定,這塊如果有小夥伴能講解的比較清楚也歡迎評論交流指導。

最後總結一下:哈希表的大小為素數時,簡單的取模哈希的結果會更加均勻,是以單從這一點上看,HashTable的哈希表大小選擇,似乎更高明些。但另一方面我們又知道,在取模計算時,如果模數是2的幂,那麼我們可以直接使用位運算來得到結果,效率要大大高于做除法。是以從hash計算的效率上,又是HashMap更勝一籌。之是以不一樣是因為HashMap用的位移運算确認具體位置,而hashTable是直接用的模。(事實就是HashMap為了加快hash的速度,将哈希表的大小固定為了2的幂。當然這引入了哈希分布不均勻的問題,是以HashMap為解決這問題,又對hash算法做了一些改動。HashMap和HashTable在計算hash時都用到了一個叫hashSeed的變量。這是因為映射到同一個hash桶内的Entry對象,是以連結清單的形式存在的,而連結清單的查詢效率比較低,是以HashMap/HashTable的效率對哈希沖突非常敏感,是以可以額外開啟一個可選hash(hashSeed),進而減少哈希沖突。)

在結尾總結性的補充一下這個hashTable和HashMap的異同:

1.首先父類不同。hashTable的父類是Dictionary<K,V>,HashMap的父類是AbstractMap<K,V>.

2.HashMap是支援null鍵和null值的,而HashTable在遇到null時,會抛出NullPointerException異常。

3.初始化大小不同,擴容機制不同。

4.hashTable為線程安全的,方法級别的強制同步。HashMap非線程安全的。是以HashMap效率性能要高。

相同點:都實作了Map<K,V>接口。