hashMap是面試中的高頻考點,或許平常工做中咱們只需把hashMap給new出來,調用put和get方法就完了。可是hashMap給咱們提供了一個絕佳的範例,展現了編程中對數據結構和算法的應用,例如位運算、hash,數組,鏈表、紅黑樹等,學習hashMap絕對是有好處的。
廢話很少說,要想學習hashMap,必先明白其數據結構。在java中,最基礎的數據結構就兩種,一種是數組,另一個就是模擬指針(引用),一塊兒來看下hashMap結構圖:
java
從類定義上看,繼承於AbstractMap,並實現Map接口,其實就是裏面定義了一些經常使用方法好比size(),isEmpty(),containsKey(),get(),put()等等,Cloneable,Serializable 的做用在以前list章節已講述過就再也不重複了,總體來講類定義仍是蠻簡單的node
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
接下來會帶領你們閱讀源碼,有些不重要的,會咔掉一部分。面試
//初始容量16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量2的30次方 static final int MAXIMUM_CAPACITY = 1 << 30; //默認加載因子,用來計算threshold static final float DEFAULT_LOAD_FACTOR = 0.75f; //鏈表轉成樹的閾值,當桶中鏈表長度大於8時轉成樹 static final int TREEIFY_THRESHOLD = 8; //進行resize操做室,若桶中數量少於6則從樹轉成鏈表 static final int UNTREEIFY_THRESHOLD = 6; //當桶中的bin樹化的時候,最小hashtable容量,最少是TREEIFY_THRESHOLD 的4倍 static final int MIN_TREEIFY_CAPACITY = 64;
//在樹化以前,桶中的單個bin都是node,實現了Entry接口 static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } }
//jdk1.8 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
hashMap中的hash算法,影響hashMap效率的重要因素之一就是hash算法的好壞。hash算法的好壞,咱們能夠簡單的經過兩個因素判斷,1是否高效2是否均勻。
你們都知道key.hashCode調用的是key鍵值類型自帶的哈希函數,返回int散列值。int值得位數有2的32次方,若是直接拿散列值做爲下標訪問hashMap主數組的話,只要hash算法比較均勻,通常是很難出現碰撞的。可是內存裝不下這麼大的數組,因此計算數組下標就採起了一種折中的辦法,就是將hash()獲得的散列值與數組長度作一個與操做。以下函數:算法
//或許你們會發現這個方法是jdk1.7,爲何不用1.8的呢?那是由於1.8裏已經去掉這個函數,直接調用,爲了講解方便,我從1.7中找出此方法方便學習 static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; return h & (length-1); }
hashMap的長度必須是2的冪次方,最小爲16.順便說一下這樣設計的好處。由於這樣(length-1)正好是一個高位掩碼,&運算會將高位置0,只保留低位數字。咱們來舉個例子,假設長度爲16,16-1=15,15的2進製表示爲
00000000 00000000 00000000 00001111.隨意定義一個散列值作&運算,結果如所示:編程
10101010 11110011 00111010 01011010 & 00000000 00000000 00000000 00001111 ------------------------------------- 00000000 00000000 00000000 00001010
也就是說實際上只截取了最低的四位,也就是咱們計算的索引結果。可是隻取後幾位的話,就算散列值分佈再均勻,hash碰撞也會很嚴重,若是hashcode函數自己很差,分佈上成等差數列的漏洞,使最後幾個低位成規律性重複,這就無比蛋疼了。這時候hash()函數的價值就體現出來了數組
h=key.hashcode() 11111011 01011111 00011100 01011011 h>>>16 ^ 00000000 00000000 11111011 01011111 ------------------------------------- 11111011 01011111 11100111 00000100 & 00000000 00000000 00000000 00001111 ------------------------------------- 00000000 00000000 00000000 00000100
(h = key.hashCode()) ^ (h >>> 16),16正好是32的一半,其目的是爲了將本身的高半區和低半區作異或,混合高低位信息,以此來加大低位的隨機性,並且混合後的低位存在部分高位的特徵,算是變相的保留了高位信息。由此看來jdk1.8對於hash算法和計算索引值的設計就基本暴露在咱們的眼前了,不得不佩服設計之巧妙。數據結構
//返回大於等於cap且距離最近的一個2的冪 //例子:cap=2 return 4; cap=9 return 16; static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
//hashMap數組,保留着鏈表的頭結點或者紅黑樹的跟結點。 //當第一次使用的時候,會對其初始化,當須要擴容時,會調用resize方法。長度必定是2的冪 transient Node<K,V>[] table; //用來遍歷的set集合,速度快於keySet transient Set<Map.Entry<K,V>> entrySet; transient int size; //用來檢測使用iterator期間結構是否發生變化,變化則觸發fail-fast機制 transient int modCount; //當容器內映射數量達到時,發生resize操做(threshold=capacity * load factor) int threshold; //加載因子,默認0.75 final float loadFactor;
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) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
hash函數以前已經研究過了,直接鎖定getNode()吧。經過hash函數算出hash值&上數組長度從而計算出索引值,而後遍歷比較key,返回對應值。併發
public V put(K key, V value) { 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; //若table爲空,則調用resize()進行初始化,並將長度賦值給n if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //根據(n-1)&hash算出索引,獲得結點p,若p爲null,則生成一個新的結點插入。 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { //若p不爲null,則將p和插入結點的key與其hash值進行比較,若相同將p的引用同時賦值給e Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //若不一樣,且結點p屬於樹節點,則調用putTreeVal() else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //若不一樣,則將p當作鏈表的頭結點,循環比較,若爲null則新增節點,且循環次數大於等於TREEIFY_THRESHOLD - 1則從鏈表結構轉爲樹結構 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; } //若鏈表中其中一結點的key與hash與插入結點一致,則跳出循環 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //若是插入的key存在,則替換舊值並返回 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //若插入的key在map中不存在,則判斷size>thresold if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } //初始化數組和擴容使用 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; } 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); } 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) { 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; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
hashMap中遍歷的方式是經過entrySet和keySet,爲了保證其效率,建議用entrySet由於他的存儲結構和hashMap一致。hashMap是如何維護entrySet的呢?經過閱讀源碼,發如今put的時候,並無對entrySet進行維護,且源碼中
entrySet方法只是new了個對象,那這個entrySet視圖的數據從哪而來呢?app
public Set<Map.Entry<K,V>> entrySet() { Set<Map.Entry<K,V>> es; return (es = entrySet) == null ? (entrySet = new EntrySet()) : es; } final class EntrySet extends AbstractSet<Map.Entry<K,V>> { public final int size() { return size; } public final void clear() { HashMap.this.clear(); } public final Iterator<Map.Entry<K,V>> iterator() { return new EntryIterator(); } } final class EntryIterator extends HashIterator implements Iterator<Map.Entry<K,V>> { public final Map.Entry<K,V> next() { return nextNode(); } } abstract class HashIterator { Node<K,V> next; // next entry to return Node<K,V> current; // current entry int expectedModCount; // for fast-fail int index; // current slot HashIterator() { expectedModCount = modCount; Node<K,V>[] t = table; current = next = null; index = 0; if (t != null && size > 0) { // advance to first entry do {} while (index < t.length && (next = t[index++]) == null); } } final Node<K,V> nextNode() { Node<K,V>[] t; Node<K,V> e = next; if (modCount != expectedModCount) throw new ConcurrentModificationException(); if (e == null) throw new NoSuchElementException(); if ((next = (current = e).next) == null && (t = table) != null) { do {} while (index < t.length && (next = t[index++]) == null); } return e; } }
經過閱讀EntrySet,咱們發現其iterator() 調用了EntryIterator(),而在對其進行實例化的時候會對其父類HashIterator進行實例化,從HashIterator的構造方法和nextNode咱們發現,其返回的視圖就是做用於table的,因此無需從新開闢內存。數據結構和算法
本篇文章主要分析hashMap的存儲結構,分析了hashMap爲何容量始終是2的冪,分析了其hash算法的好壞和影響其效率的因素,同時也瞭解到了在put和get時作了哪些操做和其中數據結構的變化。最後經過hashMap常見的遍歷方式,得出entrySet是便利效率最高的,且hashMap維護entrySet的方式。經過學習,發現hashMap的設計很是優秀,但無奈能力有限,沒法將其精妙之處所有剖析開來。下節預告:分析一下併發下的hashMap有可能形成的閉環問題和concurrentHashMap