1. Hash
把任意長度的輸入通過散列算法,變換成固定長度的輸出,該輸出就是散列值。擁有四個特性:
1. 擁有無限的輸入域和固定大小的輸出域
2. 如果輸入值相同,返回值一樣
3. 如果輸入值不相同,返回值可能相同,可能不同
4. 不同輸入值得到的哈希值,整體均勻的分布在輸出域s中——優秀哈希函數的判斷。
2. HashMap
HashMap是Node[] table哈希桶數組,其中Node是內部類,實現鍵值對Entry接口,並采用鏈表解決Hash碰撞問題。
數組默認初始長度為16,含有Loadfactor負載因子(默認為0.75),threshold=LoadFactor*length。當數組元素超過了threshold,則擴容成原來長度的兩倍,並重新對map的內容進行hash,容量總是2的冪
所以保證Hash Map高效的因素是:好的hash算法使得hash值分布均勻和擴容機制。
在JDK8以后,加入了紅黑樹,當鏈表數量超過8時使用紅黑樹。
2.1 結構實現
HashMap是數組+鏈表實現。
1)是Entry鍵值對的數組 Node[] table,即哈希桶數組,其中Node是內部類,實現鍵值對Entry接口

2)HashMap就是使用哈希表來存儲的,並且采用了鏈地址法解決沖突。
簡單來說,就是數組加鏈表的結合。在每個數組元素上都一個鏈表結構,當數據被Hash后,得到數組下標,把數據放在對應下標元素的鏈表上。
- 如果哈希桶數組很大,即使比較差的Hash算法也會比較分散
- 如果哈希桶數組很小,即使很好的Hash算法也會出現較多的碰撞
所以,需要在時間成本和空間成本作出權衡,根據實際情況確定哈希桶數組的大小,並在此基礎上設計好的hash算法減少Hash碰撞。——解決方式:Hash算法和擴容機制
2.2 擴容機制
- Node[] table初始化長度length(默認16),一般為2的冪次方(主要是為了在取模和擴容時做優化,同時為了減少沖突,HashMap定位哈希桶索引位置時,也加入了高位參與運算的過程。)
- vs HashTable:長度默認為11(素數,相對來說素數導致沖突的概率小於合數)
- Load factor負載因子(默認是0.75)
- threshold是Hash Map所能容納的最大數據量的Node鍵值對。threshold = length*loadfactor
- 當元素數量超過了threshold,那么就擴容resize,擴容后的容量是原來的兩倍,並且重新計算每個元素在數組中的位置。
注意: 擴容是一個特別耗性能的操作,所以當程序員在使用HashMap的時候,估算map的大小,初始化的時候給一個大致的數值,避免map進行頻繁的擴容。
2.3 功能實現
1. 確定哈希桶數組索引位置
Hash算法本質上就是三步:1)取key的HashCode值;2)高位運算;3)取模運算
方法一:把Hash值對數組取模運算。——時間消耗大
方法二:hash&(table.length-1)來得到該對象的保存位,當長度總是為2的冪次方時,相當於對length取模,但是&比%更高效。
在JDK1.8的實現中,優化了高位運算的算法,通過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質量來考慮的,這么做可以在數組table的length比較小的時候,也能保證考慮到高低Bit都參與到Hash的計算中,同時不會有太大的開銷。
2. HashMap的Put方法:
1. 首先根據鍵值key計算hash值,得到索引值i。
- 如果table[i]=null,直接新建節點添加。並判斷元素的數量是否超過threshold,
- 如果table[i] !=null,則在該索引對應的鏈表中插入該元素
2. 判斷數組中的元素數目是否超過thrshold,
- 如果超過,則擴容resize成原來的兩倍,並重新計算各元素的數組索引值。
此外:
(1) 負載因子是可以修改的,也可以大於1,但是建議不要輕易修改,除非情況非常特殊。
(2) HashMap是線程不安全的,不要在並發的環境中同時操作HashMap,建議使用ConcurrentHashMap。
(3) JDK1.8引入紅黑樹大程度優化了HashMap的性能。
https://zhuanlan.zhihu.com/p/21673805
3. 其他結構
1. HashMap:
- 根據鍵的hashcode值來存儲數據,大多數情況下可以直接定位到它的值,因而具有很快的訪問速度,但遍歷順序確實不確定的。
- HashMap最多只允許一條記錄的鍵為null,允許多條記錄的值為null。
- 非線程安全。即任一時刻可以有多個線程同時寫HashMap,可能會導致數據的不一致。
- 若要線程安全:synchronizedMap、ConcurrentHashMap
2. HashTable
是遺留類,很多映射的常用功能和Hash Map類似,不同的是它承自Dictionary類,是線程安全的。任意時刻只能有一個線程能夠寫HashTable,並發性不如ConcurrentHashMap,因為ConcurrentHashMap引入了分段鎖。
一般不建議在新代碼中使用
3. TreeMap
實現SortedMap接口,能夠把它保存的記錄按照鍵排序,默認是按鍵值的升序排序,也可以指定排序的比較器。在使用TreeMap時,key必須實現Comparable接口或者在構造TreeMap傳入自定義的Comparator,否則會在運行時拋出java.lang.ClassCastException類型的異常。
4. LinkedHashMap
是Hash Map的一個字類,保存了記錄的插入順序,在使用Iterator遍歷LinkedhashMap時,先得到的記錄肯定是先插入的,也可以在構造時帶參數,按照訪問次序排序。
HashMap VS HashTable
1. HashMap
- Entry[] table; - size>=threshold就會擴容,threshold等於capacity*loadFactor - HashMap默認的初始容量是16,負荷系數是0.75。
擴容會對map的內容進行重新哈希,容量總是2的冪。 -
數組第一個位置放得是key=null的元素 - reHash()時: int i = indexFor(e.hash, newCapacity); 然后頭插法插入新數組 -自定義對象重寫equals()時也要重寫hashcode();使用不可變類當key;原因都是怕存入Hashmap之后get(key)找不到元素。 - jdk1.8 鏈表數量超過8時改用紅黑樹。node<k,v>[] table;node是內部類,實現Map.Entry接口{K key; V value; node<K,V> next; int hash} - jdk1.8 擴容不需要重新計算hash,因為容量擴大兩倍, 那么n-1的mask范圍在高位多1bit, 只需要看看原來元素的hash值新增的那個bit是1還是0,是0的元素位置沒變,是1的話索引變成“原索引+舊容量(即擴大的容量)” - jdk1.8中rehash的時候,舊鏈表遷移新鏈表的時候,如果在新表的數組索引位置相同,則鏈表元素bu不會倒置 - jdk1.8舊鏈表rehash時新建兩條鏈表AB,如果 (e.hash&oldCap)==0是指元素位置不變,尾插法插入鏈表A,最后B鏈表插入 原索引+oldCap放到bucket里 Hashtable - Entry<K,V>{final K key; V value; Entry<K,V> next;final int hash} - 計算哈希值:int hash = hash(key.hashCode()); - 計算數組落位:hash & (capacity-1),其中capacity是2的冪 - 新增加的Entry<K,V>放在鏈表頭部 - 擴容:使用新的容量創建新數組,將每一個元素重新計算重新落位,重新計算臨界值 - 查找value必須遍歷整個數組遍歷每個位置上的整條鏈表 - Hashtable不允許鍵或者值是null。 - Hashtable默認數組大小11,增長old*2+1,HashMap默認數組大小16,增長2的指數。 - Hashtablehash值直接使用hashCode
