來,進來的小夥伴們,咱們認識一下。html
我是俗世遊子,在外流浪多年的Java程序猿java
前面兩篇咱們聊了聊關於集合容器中的List集合,其中包含兩個子類:node
若是尚未看過的小夥伴,能夠先返回去看看面試
我在LinkedList中留下了幾個思考問題,不知道有沒有想到的,咱們在評論區裏探討探討啊api
這篇咱們聊一聊Map,咱們再回顧一下,前面介紹的時候,咱們說過,Map和List很大的一個區別在於:數組
在傳統的系統中,咱們的數據大概10W,百W的存儲就足夠了,可是在一些特殊的應用或者大數據平臺中,涉及到千萬甚至更多的數據,安全
本人在上家公司開發的廣告投放系統中,天天至少會產生2000W的數據數據結構
這時若是咱們想從其中查找到某一條數據就很是麻煩,涉及到性能等的問題,而經過K,V
形式存儲,咱們就至關於對某一個值添加了索引,經過這個索引咱們就能很快定位到數據,提升系統的性能。oracle
關於K,V
形式的存儲,咱們在工做中還會用到如:less
好,瞭解到這一點以後,咱們繼續日後看。
前面咱們講到,集合中全部的父類是Collection,可是Map是單獨的一套接口,這裏不能混在一塊兒,下面咱們來看看Map的實現子類:
瞭解ArrayList中我也給出了一張思惟導圖
咱們就一個一個來看
最重要的一點:面試出場率賊高了【9月份面試一個月,80%的公司都有問到(當時不懂啊-_-||)】
這裏,在聊今天的主角:HashMap以前,咱們先來簡單的認識一下什麼是哈希表
HashMap很長,文采略爛,你們要有耐心哦
也叫散列表,是根據關鍵碼值(Key value)而直接進行訪問的一種數據結構。也就是說,它經過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度
來源:百度百科
PS:概念都不是人話,不用記他,直接看結構
哈希表分爲多種類型,下面咱們來看到的是在HashMap底層實現的結構:
上面是一個數組,在內存中是一塊連續的內存空間,Key值取到hashCode碼
而後再對數組長度作取模操做,獲得對應的下標位置,而後將Value值放到對應下標位置的地方;若是在對應下標位置的地方存在元素,那麼就已鏈表的形式追加
這種方式在散列函數中稱爲:除法散列法
取值也是同樣:經過關鍵key的哈希取到對應下標以後,若是對應位置只有一個數據,那麼就直接取出,不然就在鏈表中進行比對而後再取出對應的數據
簡單對哈希表認識一下,咱們繼續聊HashMap
背景說明:JDK1.8
老規矩,咱們來先來看一看HashMap的一個類分佈圖
咱們都是這樣來構造HashMap的:
Map<String, String> hashMap = new HashMap<>(); Map<String, String> hashMap = new HashMap<>(20);
這樣一個無參的構造方式,咱們來看看具體作了什麼?
/**MUST be a power of two.*/ // 默認初始化長度 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 負載因子,負責斷定數據存儲到什麼地步的時候進行擴容 static final float DEFAULT_LOAD_FACTOR = 0.75f; public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
按照咱們以前的經驗,存儲數據須要在內存中開闢空間,可是在HashMap的構造方法中並無這麼作,包括其餘有參的構造方法:
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } // 除外,傳遞參數不一樣, public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
這裏值得咱們借鑑:若是直接在定義構造方法的時候就在內存中開闢空間的話,若是不存儲數據的話,那麼這塊內存不就被浪費了麼
前面咱們都知道,在定義數組的時候須要指定數組的長度,而哈希表中有采用數組的結構,那麼若是不定義指定長度的話,默認的一個長度就是屬性值:DEFAULT_INITIAL_CAPACITY
,等於 16。
咱們重點還要關注它的註釋:MUST be a power of two WHY?
在Hashtable中,是按照除法散列法中的規範來作的:也就是上面說的不太接近2的整數冪的素數,可是爲何在HashMap中就沒有采用這種規範,而是要採用2的N次冪呢?咱們後面再具體說
一樣,咱們還要在關注一個點:
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; }
經過計算,獲得離傳遞參數最近的2的N次冪的數,因此說,哪怕傳遞的參數不知足規定,在代碼中也會幫咱們進行調整
一樣在HashMap中還存在一個屬性loadFactor
:表示負載因子,簡單來講就是該屬性決定了HashMap容器空間何時該擴容,默認是0.75
好比:初始長度爲16,負載因子是0.75,那麼當容器中存儲了> (16 * 0.75 = 12)的時候,就會進行擴容操做
下面咱們再來看兩個屬性
static final int TREEIFY_THRESHOLD = 8; static final int UNTREEIFY_THRESHOLD = 6;
這兩個屬性值是在JDK1.8以後加進來的,簡單來講就是:當鏈表長度>8的時候,鏈表會轉成紅黑樹的結構存儲數據,當紅黑樹的節點<6個的時候,會轉成鏈表的形式
也就是下面的結構:
關於爲何是8轉紅黑樹?
在屬性值
DEFAULT_INITIAL_CAPACITY
上面有一段註釋,給出了分析:/* * the frequency of * nodes in bins follows a Poisson distribution * (http://en.wikipedia.org/wiki/Poisson_distribution) with a * parameter of about 0.5 on average for the default resizing * threshold of 0.75, although with a large variance because of * resizing granularity. Ignoring variance, the expected * occurrences of list size k are (exp(-0.5) * pow(0.5, k) / * factorial(k)). The first values are: * * 0: 0.60653066 * 1: 0.30326533 * 2: 0.07581633 * 3: 0.01263606 * 4: 0.00157952 * 5: 0.00015795 * 6: 0.00001316 * 7: 0.00000094 * 8: 0.00000006 * more: less than 1 in ten million * */也就是說,經過計算,在k=8的時候,接近於0,因此定義爲8,提高檢索的效率,這裏涉及到一個叫作《泊松分佈》:這是一種統計與機率學裏常見到的離散機率分佈
介紹的話推薦你們看這一篇:如何通俗理解泊松分佈
瞭解完基本的屬性值以後,咱們來看具體的操做方法,仍是同樣的,在此以前咱們來看兩個類:
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; } // ... }
這個類你們確定不陌生,前面在聊LinkedList的時候就已經見過了,不過這個是單向鏈表的方式
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red; TreeNode(int hash, K key, V val, Node<K,V> next) { super(hash, key, val, next); } //... }
這是關於紅黑樹的具體類
下面咱們繼續,
咱們是這樣調用的:
hashMap.put("key1", "value1"); public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
前面也說到,肯定關鍵值Key在數組中的位置,那麼咱們先來看看是如何進行hash運算的:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
爲了可以讓計算出來索引位置更分散,因此先(h >>> 16)
,一樣,再經過 ^
運算讓哈希值的高低位都能參與運算,從而減小哈希碰撞的概率
擾動函數
下面咱們看具體的實現:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 註釋1 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 註釋2 else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 註釋4 e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);// 註釋5.2 else { for (int binCount = 0; ; ++binCount) { // 鏈表的插入過程 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // 註釋5 treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // 註釋4.2 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) // 註釋3 resize(); afterNodeInsertion(evict); return null; }
前面說到,在構造方法中什麼都沒作,只有在實際添加元素的時候纔會開闢空間。在上面註釋1的地方就是開闢空間的過程,同時在註釋3的地方,該方法也是咱們擴容的過程,調用的都是同一個方法resize()
:
一步步來,先剖析
putVal()
方法,而後咱們在來看resize()
在第一次put()
,只是建立了一個Node數組
,沒有其餘操做, 也就是在內存中開闢空間,而後咱們繼續往下看
第一次put()
元素,那麼確定會進入到註釋2的位置,經過 &
來計算當前元素所處的下標位置並賦值
同時,若是以前有看過JDK1.7
的源碼的話,會發如今1.7中有這樣一個方式:
static int indexFor(int h, int length) { return h & (length - 1); }
在JDK1.8
中:
i = (n - 1) & hash
舉個例子:
(n-1)以後的二進制: 01111
hash = 18,轉成二進制: 10010
&運算以後: 00010
經過計算,i=2
在設置成2的N次冪以後,在計算下標位置的時候能夠保證(n-1)的後幾位必定是1,方便進行 &
運算,而&
的效率要高於%
運算。
多用用位運算符,那不是擺設。O(∩_∩)O
繼續調用put()
添加元素,註釋4和註釋4.2是相輔相成的,走到這裏會判斷key是否存在,若是存在key,那麼e仍是會獲得從HashMap中數組索引位置上獲得的key,在註釋4.2的地方將value進行賦值操做,也就是覆蓋原先的值並返回舊值。
也就是說:在HashMap,不存在重複元素,若是在同一個key上存儲了多個元素,那麼只會存儲最新的元素
hashMap.put("key1", "value1"); hashMap.put("key1", "value2"); System.out.println(hashMap); // {key1=value2}
前面咱們也說過,HashMap在JDK1.8的版本中:當鏈表長度>8的時候會將存儲結構轉成紅黑樹來存儲,那麼在註釋5的地方咱們就獲得了驗證,一樣,咱們來看一下轉換過程:
final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); } }
前面咱們已經看過TreeNode
這個類,這樣也就對應到了咱們上面的底層結構圖
同時咱們能夠看到註釋5.2的地方,若是存儲已經轉換成紅黑樹的形式,那麼就對紅黑樹進行插入操做
紅黑樹後面聊
接下來咱們看resize()
,上面知道初始化的時候是建立數組的過程,那麼咱們看有值的時候作了什麼事情
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; // oldCap = 16 int oldThr = threshold; // oldThr = 12 int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && // newCap = 32 oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold // newThr = 24 } 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; }
後面註釋舉了個小栗子。咱們也能夠看到
在JDK1.8
中數據遷移是判斷:當前hash & 舊容器長度的結果:
具體邏輯在這裏:
// e.hash = 65 經過計算爲0, if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } // e.hash = 6366 經過計算不爲0,走else else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } // 原數組中的位置和新數組中位置相同 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } // 新數組中的位置改變 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; }
HashMap關鍵點也就聊完了,下面咱們來總結一下
&
計算,性能對比%
更高更多關於HashMap使用方法推薦查看其文檔: