以前從java的集合接口入手開始看,一臉懵逼。最近直接看了網上的文章Hashmap的工做原理及實現,纔對hashmap的原理有所瞭解。html
從本文你能夠學習到:java
- 何時會使用HashMap?他有什麼特色?
- 你知道HashMap的工做原理嗎?
- 你知道get和put的原理嗎?equals()和hashCode()的都有什麼做用?
- 你知道hash的實現嗎?爲何要這樣實現?
- 若是HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?
當咱們執行下面的操做時:node
1 2 3 4 5 6 7 8 9 10 11 12 |
HashMap<String, Integer> map = new HashMap<String, Integer>(); map.put("語文", 1); map.put("數學", 2); map.put("英語", 3); map.put("歷史", 4); map.put("政治", 5); map.put("地理", 6); map.put("生物", 7); map.put("化學", 8); for(Entry<String, Integer> entry : map.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); } |
運行結果是數組
政治: 5
生物: 7
歷史: 4
數學: 2
化學: 8
語文: 1
英語: 3
地理: 6app
發生了什麼呢?下面是一個大體的結構,但願咱們對HashMap的結構有一個感性的認識:
ide
在官方文檔中是這樣描述HashMap的:函數
Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. (The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.性能
幾個關鍵的信息:基於Map接口實現、容許null鍵/值、非同步、不保證有序(好比插入的順序)、也不保證序不隨時間變化。學習
在HashMap中有兩個很重要的參數,容量(Capacity)和負載因子(Load factor)測試
- Initial capacity The capacity is the number of buckets in the hash table, The initial capacity is simply the capacity at the time the hash table is created.
- Load factor The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased.
簡單的說,Capacity就是bucket的大小,Load factor就是bucket填滿程度的最大比例。若是對迭代性能要求很高的話不要把capacity
設置過大,也不要把load factor
設置太小。當bucket中的entries的數目大於capacity*load factor
時就須要調整bucket的大小爲當前的2倍。
put函數大體的思路爲:
具體代碼的實現以下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
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; // 超過load factor*current capacity,resize if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } |
在理解了put以後,get就很簡單了。大體思路以下:
具體代碼的實現以下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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) { // 在樹中get if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); // 在鏈表中get do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; } |
在get和put的過程當中,計算下標時,先對hashCode進行hash操做,而後再經過hash值進一步計算下標,以下圖所示:
在對hashCode()計算hash時具體實現是這樣的:
1 2 3 4 |
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } |
能夠看到這個函數大概的做用就是:高16bit不變,低16bit和高16bit作了一個異或。其中代碼註釋是這樣寫的:
Computes key.hashCode() and spreads (XORs) higher bits of hash to lower. Because the table uses power-of-two masking, sets of hashes that vary only in bits above the current mask will always collide. (Among known examples are sets of Float keys holding consecutive whole numbers in small tables.) So we apply a transform that spreads the impact of higher bits downward. There is a tradeoff between speed, utility, and quality of bit-spreading. Because many common sets of hashes are already reasonably distributed (so don’t benefit from spreading), and because we use trees to handle large sets of collisions in bins, we just XOR some shifted bits in the cheapest possible way to reduce systematic lossage, as well as to incorporate impact of the highest bits that would otherwise never be used in index calculations because of table bounds.
在設計hash函數時,由於目前的table長度n爲2的冪,而計算下標的時候,是這樣實現的(使用&
位操做,而非%
求餘):
(n - 1) & hash
設計者認爲這方法很容易發生碰撞。爲何這麼說呢?不妨思考一下,在n - 1爲15(0x1111)時,其實散列真正生效的只是低4bit的有效位,固然容易碰撞了。
所以,設計者想了一個顧全大局的方法(綜合考慮了速度、做用、質量),就是把高16bit和低16bit異或了一下。設計者還解釋到由於如今大多數的hashCode的分佈已經很不錯了,就算是發生了碰撞也用O(logn)
的tree去作了。僅僅異或一下,既減小了系統的開銷,也不會形成的由於高位沒有參與下標的計算(table長度比較小時),從而引發的碰撞。
若是仍是產生了頻繁的碰撞,會發生什麼問題呢?做者註釋說,他們使用樹來處理頻繁的碰撞(we use trees to handle large sets of collisions in bins),在JEP-180中,描述了這個問題:
Improve the performance of java.util.HashMap under high hash-collision conditions by using balanced trees rather than linked lists to store map entries. Implement the same improvement in the LinkedHashMap class.
以前已經提過,在獲取HashMap的元素時,基本分兩步:
在Java 8以前的實現中是用鏈表解決衝突的,在產生碰撞的狀況下,進行get時,兩步的時間複雜度是O(1)+O(n)。所以,當碰撞很厲害的時候n很大,O(n)的速度顯然是影響速度的。
所以在Java 8中,利用紅黑樹替換鏈表,這樣複雜度就變成了O(1)+O(logn)了,這樣在n很大的時候,可以比較理想的解決這個問題,在Java 8:HashMap的性能提高一文中有性能測試的結果。
當put時,若是發現目前的bucket佔用程度已經超過了Load Factor所但願的比例,那麼就會發生resize。在resize的過程,簡單的說就是把bucket擴充爲2倍,以後從新計算index,把節點再放到新的bucket中。resize的註釋是這樣描述的:
Initializes or doubles table size. If null, allocates in accord with initial capacity target held in field threshold. Otherwise, because we are using power-of-two expansion, the elements from each bin must either stay at same index, or move with a power of two offset in the new table.
大體意思就是說,當超過限制的時候會resize,然而又由於咱們使用的是2次冪的擴展(指長度擴爲原來2倍),因此,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置。
怎麼理解呢?例如咱們從16擴展爲32時,具體的變化以下所示:
所以元素在從新計算hash以後,由於n變爲2倍,那麼n-1的mask範圍在高位多1bit(紅色),所以新的index就會發生這樣的變化:
所以,咱們在擴充HashMap的時候,不須要從新計算hash,只須要看看原來的hash值新增的那個bit是1仍是0就行了,是0的話索引沒變,是1的話索引變成「原索引+oldCap」。能夠看看下圖爲16擴充爲32的resize示意圖:
這個設計確實很是的巧妙,既省去了從新計算hash值的時間,並且同時,因爲新增的1bit是0仍是1能夠認爲是隨機的,所以resize的過程,均勻的把以前的衝突的節點分散到新的bucket了。
下面是代碼的具體實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { // 超過最大值就再也不擴充了,就只好隨你碰撞去吧 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 沒超過最大值,就擴充爲原來的2倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 計算新的resize上限 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { // 把每一個bucket都移動到新的buckets中 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; // 原索引 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } // 原索引+oldCap else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 原索引放到bucket裏 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } // 原索引+oldCap放到bucket裏 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; } |
咱們如今能夠回答開始的幾個問題,加深對HashMap的理解:
1. 何時會使用HashMap?他有什麼特色?
是基於Map接口的實現,存儲鍵值對時,它能夠接收null的鍵值,是非同步的,HashMap存儲着Entry(hash, key, value, next)對象。
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. 你知道get和put的原理嗎?equals()和hashCode()的都有什麼做用?
經過對key的hashCode()進行hashing,並計算下標( n-1 & hash),從而得到buckets的位置。若是產生碰撞,則利用key.equals()方法去鏈表或樹中去查找對應的節點
4. 你知道hash的實現嗎?爲何要這樣實現?
在Java 1.8的實現中,是經過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16)
,主要是從速度、功效、質量來考慮的,這麼作能夠在bucket的n比較小的時候,也能保證考慮到高低bit都參與到hash的計算中,同時不會有太大的開銷。
5. 若是HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?
若是超過了負載因子(默認0.75),則會從新resize一個原來長度兩倍的HashMap,而且從新調用hash方法。
關於Java集合的小抄中是這樣描述的:
以Entry[]數組實現的哈希桶數組,用Key的哈希值取模桶數組的大小可獲得數組下標。
插入元素時,若是兩條Key落在同一個桶(好比哈希值1和17取模16後都屬於第一個哈希桶),Entry用一個next屬性實現多個Entry以單向鏈表存放,後入桶的Entry將next指向桶當前的Entry。
查找哈希值爲17的key時,先定位到第一個哈希桶,而後以鏈表遍歷桶裏全部元素,逐個比較其key值。
當Entry數量達到桶數量的75%時(不少文章說使用的桶數量達到了75%,但看代碼不是),會成倍擴容桶數組,並從新分配全部原來的Entry,因此這裏也最好有個預估值。
取模用位運算(hash & (arrayLength-1))會比較快,因此數組的大小永遠是2的N次方, 你隨便給一個初始值好比17會轉爲32。默認第一次放入元素時的初始值是16。
iterator()時順着哈希桶數組來遍歷,看起來是個亂序。
在JDK8裏,新增默認爲8的閥值,當一個桶裏的Entry超過閥值,就不以單向鏈表而以紅黑樹來存放以加快Key的查找速度。
HashMap的工做原理
Java 8:HashMap的性能提高
JEP 180: Handle Frequent HashMap Collisions with Balanced Trees
ConurrentHashMap和Hashtable的區別
HashMap和Hashtable的區別