原文地址java
HashMap
是 Map
的一個實現類,它表明的是一種鍵值對的數據存儲形式。node
大多數狀況下能夠直接定位到它的值,於是具備很快的訪問速度,但遍歷順序倒是不肯定的。算法
HashMap
最多隻容許一條記錄的鍵爲null
,容許多條記錄的值爲null
。不保證有序(好比插入的順序)、也不保證序不隨時間變化。數組
jdk 8
以前,其內部是由數組+鏈表來實現的,而 jdk 8
對於鏈表長度超過 8 的鏈表將轉儲爲紅黑樹。安全
HashMap
非線程安全,即任一時刻能夠有多個線程同時寫HashMap
,可能會致使數據的不一致。若是須要知足線程安全,能夠用 Collections
的synchronizedMap
方法使HashMap
具備線程安全的能力,或者使用ConcurrentHashMap
。app
下面咱們先來看一下HashMap內部所用到的存儲結構函數
HashMap
是數組+鏈表+紅黑樹(JDK1.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; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }
Node
是HashMap
的一個內部類,實現了Map.Entry接口,本質上就是一個映射(鍵值對)。優化
有時兩個key
會定位到相同的位置,表示發生了Hash碰撞。固然Hash
算法計算結果越分散均勻,Hash
碰撞的機率就越小,map
的存取效率就會越高。this
HashMap
類中有一個很是重要的字段,就是 Node[] table
,即哈希桶數組。
若是哈希桶數組很大,即便較差的Hash
算法也會比較分散,若是哈希桶數組數組很小,即便好的Hash
算法也會出現較多碰撞。
因此就須要在空間成本和時間成本之間權衡,其實就是在根據實際狀況肯定哈希桶數組的大小,並在此基礎上設計好的hash
算法減小Hash碰撞。那麼經過什麼方式來控制map使得Hash碰撞的機率又小,哈希桶數組(Node[] table)佔用空間又少呢?答案就是好的Hash算法和擴容機制。
下面咱們就來看一下hashmap
中通過jdk1.8優化過的Hash算法和擴容機制。
不過在這以前咱們先了解下hashmap中的變量
//初始化容量16 hashMap的容量必須是2的指數倍,Hashtable是11 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //最大容量2的30次方 static final int MAXIMUM_CAPACITY = 1 << 30; //默認加載因子默認的平衡因子爲0.75,這是權衡了時間複雜度與空間複雜度以後的最好取值 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 若是鏈表的長度超過這個閾值就改用紅黑樹存儲 static final int TREEIFY_THRESHOLD = 8; static final int UNTREEIFY_THRESHOLD = 6; static final int MIN_TREEIFY_CAPACITY = 64; transient Node<K,V>[] table; transient Set<Map.Entry<K,V>> entrySet; transient int size; //實際存儲的鍵值對個數 transient int modCount; //閾值,當table == {}時,該值爲初始容量(初始容量默認爲16);當table被填充了,也就是爲table分配內存空間後,threshold通常爲 capacity*loadFactory。 int threshold; final float loadFactor; //負載因子,表明了table的填充度有多少,默認是0.75
在HashMap中有兩個很重要的參數,容量(Capacity)和負載因子(Load factor)
Capacity
就是buckets
的數目,Load factor
就是buckets
填滿程度的最大比例。若是對迭代性能要求很高的話不要把capacity
設置過大,也不要把load factor
設置太小。當bucket
填充的數目(即hashmap中元素的個數)大於capacity*load factor
時就須要調整buckets的數目爲當前的2倍。
static final int hash(Object key) { int h; // h = key.hashCode() 爲第一步 取hashCode值 // h ^ (h >>> 16) 爲第二步 高位參與運算 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } static int indexFor(int h, int length) { return h & (length-1); //第三步 取模運算 }
indexFor是jdk1.7的源碼,jdk1.8沒有這個方法可是jdk1.8也是經過取模運算來計算的
這裏的Hash算法本質上就是三步:取key的hashCode值、高位運算、取模運算。
對於任意給定的對象,只要它的hashCode()
返回值相同,那麼程序調用方法一所計算獲得的Hash
碼值老是相同的。咱們首先想到的就是把hash
值對數組長度取模運算,這樣一來,元素的分佈相對來講是比較均勻的。可是,模運算的消耗仍是比較大的,這裏咱們用&位運算來優化效率。
這個方法很是巧妙,它經過h & (table.length -1)
來獲得該對象的保存位,而HashMap
底層數組的長度老是2的n次方,這是HashMap
在速度上的優化。當length老是2的n次方時,h& (length-1)
運算等價於對length取模,也就是h%length
,可是&比%具備更高的效率。
在JDK1.8
的實現中,優化了高位運算的算法,經過hashCode()
的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質量來考慮的,這麼作能夠Node數組table的length比較小的時候,也能保證考慮到高低Bit都參與到Hash的計算中,同時不會有太大的開銷。
擴容(resize)就是從新計算容量,向HashMap
對象裏不停的添加元素,而HashMap
對象內部的數組沒法裝載更多的元素時,對象就須要擴大數組的長度,以便能裝入更多的元素。
固然Java
裏的數組是沒法自動擴容的,方法是使用一個新的數組代替已有的容量小的數組,就像咱們用一個小桶裝水,若是想裝更多的水,就得換大水桶。
當put
時,若是發現目前的bucket
佔用程度已經超過了Load Factor
所但願的比例,那麼就會發生resize
。在resize
的過程,簡單的說就是把bucket
擴充爲2倍,以後從新計算index
,把節點再放到新的bucket
中。
由於咱們使用的是2次冪的擴展(指長度擴爲原來2倍),因此,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置。
例如咱們從16擴展爲32時,具體的變化以下所示:
所以元素在從新計算hash
以後,由於n變爲2倍,那麼n-1的mask範圍在高位多1bit(紅色),所以新的index就會發生這樣的變化:
所以,咱們在擴充HashMap
的時候,不須要從新計算hash
,只須要看看原來的hash
值新增的那個bit是1仍是0就行了,是0的話索引沒變,是1的話索引變成「原索引+oldCap」。
這個設計確實很是的巧妙,既省去了從新計算hash值的時間,並且同時,因爲新增的1bit是0仍是1能夠認爲是隨機的,所以resize
的過程,均勻的把以前的衝突的節點分散到新的bucket
了。
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) { 若是容量超過Hash Map限定的最大值,將再也不擴容 threshold = Integer.MAX_VALUE; return oldTab; } // 沒超過最大值,就擴充爲原來的2倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // 2倍 } //數組未初始化,但閾值不爲 0,爲何不爲 0 ? //構造函數根據傳入的容量打造了一個合適的數組容量暫存在閾值中,這裏直接使用 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { //數組未初始化而且閾值也爲0,說明一切都以默認值進行構造 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // newCap = oldThr 以後並無計算閾值,因此 newThr = 0 // 從新計算下一次進行擴容的上限 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) { // 把每一個bucket都移動到新的buckets中 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) //若是 e 是紅黑樹結點,紅黑樹分裂,轉移至新表 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { //這部分是將鏈表中的各個節點原序地轉移至新表中 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 { // 原索引+oldCap if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { // 原索引放到bucket裏 loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { // 原索引+oldCap放到bucket裏 hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
下面咱們再來看看hashmap中的其餘方法
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); }
這是一個最基本的構造函數,須要調用方傳入兩個參數,initialCapacity 和 loadFactor。
程序的大部分代碼在判斷傳入參數的合法性,initialCapacity 小於零將拋出異常,大於 MAXIMUM_CAPACITY 將被限定爲 MAXIMUM_CAPACITY。loadFactor 若是小於等於零或者非數字類型也會拋出異常。
整個構造函數的核心在對 threshold 的初始化操做:
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
實例時,若是給定了initialCapacity
,因爲HashMap
的capacity
都是2的冪次方,所以這個方法用於找到大於等於initialCapacity
的最小的2的冪(initialCapacity若是就是2的冪,則返回的仍是這個數)。
下面分析這個算法:
首先,咱們想一下爲何要對cap作減1操做?
int n = cap - 1
這是爲了防止,cap已是2的冪。若是cap已是2的冪,又沒有執行這個減1操做,則執行完後面的幾條無符號右移操做以後,返回的capacity將是這個cap的2倍。若是不懂,要看完後面的幾個無符號右移以後再回來看看。
下面看看這幾個無符號右移操做:
若是n這時爲0了(通過了cap-1以後),則通過後面的幾回無符號右移依然是0,最後返回的capacity是1(最後有個n+1的操做)。
這裏咱們只討論n不等於0的狀況。
n |= n >>> 1;
因爲n不等於0,則n的二進制表示中總會有一bit爲1,這時考慮最高位的1。經過無符號右移1位,則將最高位的1右移了1位,再作或操做,使得n的二進制表示中與最高位的1緊鄰的右邊一位也爲1,如000011xxxxxx。
n |= n >>> 2;
注意,這個n已經進行過 n |= n >>> 1; 操做。假設此時n爲000011xxxxxx ,則n無符號右移兩位,會將最高位兩個連續的1右移兩位,而後再與原來的n作或操做,這樣n的二進制表示的高位中會有4個連續的1。如00001111xxxxxx 。
n |= n >>> 4;
此次把已經有的高位中的連續的4個1,右移4位,再作或操做,這樣n的二進制表示的高位中會有8個連續的1。如00001111 1111xxxxxx 。
以此類推 。。。
注意,容量最大也就是32bit的正數,所以最後 n |= n >>> 16; 最多也就32個1,可是這時已經大於了MAXIMUM_CAPACITY ,因此取值到MAXIMUM_CAPACITY 。
下面咱們經過一個圖片來看一下整個過程:
HashMap 中還有不少的重載構造函數,但幾乎都是基於上述的構造函數的。
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
以上這些構造函數都沒有直接的建立一個切實存在的數組,他們都是在爲建立數組須要的一些參數作初始化,
因此有些在構造函數中並無被初始化的屬性都會在實際初始化數組的時候用默認值替換。
實際對數組進行初始化是在添加元素的時候進行的(即put方法)
public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
put
方法也是HashMap
中比較重要的方法,由於經過該方法咱們能夠窺探到 HashMap
在內部是如何進行數據存儲的,所謂的數組+鏈表+紅黑樹的存儲結構是如何造成的,又是在何種狀況下將鏈表轉換成紅黑樹來優化性能的。
put方法的大體實現過程以下:
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; if ((tab = table) == null || (n = tab.length) == 0) // tab爲空則建立(初次添加元素) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) //根據鍵值key計算hash值獲得插入的數組索引i,若是table[i]==null,直接新建節點添加 tab[i] = newNode(hash, key, value, null); else { //若是對應的節點存在元素 Node<K,V> e; K k; if (p.hash == hash && //判斷table[i]的首個元素是否和key同樣,若是相同直接覆蓋value ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) //判斷table[i] 是否爲treeNode,即table[i] 是不是紅黑樹,若是是紅黑樹,則直接在樹中插入鍵值對 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 遍歷table[i],判斷鏈表長度是否大於TREEIFY_THRESHOLD(默認值爲8),大於8的話把鏈表轉換爲紅黑樹,在紅黑樹中執行插入操做,不然進行鏈表的插入操做; // 遍歷過程當中若發現key已經存在直接覆蓋value便可; 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; } } //e 不是 null,說明當前的 put 操做是一次修改操做而且e指向的就是須要被修改的結點 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; // 插入成功後,判斷實際存在的鍵值對數量size是否超多了最大容量threshold,若是超過,進行擴容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
在理解了put以後,get就很簡單了。大體思路以下:
bucket裏的第一個節點,直接命中;
若是有衝突,則經過key.equals(k)去查找對應的entry
若爲樹,則在樹中經過key.equals(k)查找,O(logn);
若爲鏈表,則在鏈表中經過key.equals(k)查找,O(n)。
具體代碼的實現以下:
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; }
刪除操做就是一個查找+刪除的過程,相對於添加操做其實容易一些
public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; }
根據鍵值刪除指定節點,這是一個最多見的操做了。顯然,removeNode 方法是核心。
final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) { if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); else if (node == p) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }
刪除操做須要保證在表不爲空的狀況下進行,而且 p 節點根據鍵的 hash 值對應到數組的索引,在該索引處一定有節點,若是爲 null ,那麼間接說明此鍵所對應的結點並不存在於整個 HashMap 中,這是不合法的,因此首先要在這兩個大前提下才能進行刪除結點的操做。
第一步
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k)))) node = p;
須要刪除的結點就是這個頭節點,讓 node 引用指向它。不然說明待刪除的結點在當前 p 所指向的頭節點的鏈表或紅黑樹中,因而須要咱們遍歷查找。
第二步
else if ((e = p.next) != null) { if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { do { if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } }
若是頭節點是紅黑樹結點,那麼調用紅黑樹本身的遍歷方法去獲得這個待刪結點。不然就是普通鏈表,咱們使用 do while 循環去遍歷找到待刪結點。找到節點以後,接下來就是刪除操做了。
第三步
if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) { if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); else if (node == p) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; }
刪除操做也很簡單,若是是紅黑樹結點的刪除,直接調用紅黑樹的刪除方法進行刪除便可,若是是待刪結點就是一個頭節點,那麼用它的 next 結點頂替它做爲頭節點存放在 table[index] 中,若是刪除的是普通鏈表中的一個節點,用該結點的前一個節點直接跳過該待刪結點指向它的 next 結點便可。
最後,若是 removeNode 方法刪除成功將返回被刪結點,不然返回 null。
public void clear() { Node<K,V>[] tab; modCount++; if ((tab = table) != null && size > 0) { size = 0; for (int i = 0; i < tab.length; ++i) tab[i] = null; } }
該方法調用結束後將清除 HashMap 中存儲的全部元素。
//實例屬性 keySet transient volatile Set<K> keySet; public Set<K> keySet() { Set<K> ks; return (ks = keySet) == null ? (keySet = new KeySet()) : ks; } final class KeySet extends AbstractSet<K> { public final int size() { return size; } public final void clear() { HashMap.this.clear(); } public final Iterator<K> iterator() { return new KeyIterator(); } public final boolean contains(Object o) { return containsKey(o); } public final boolean remove(Object key) { return removeNode(hash(key), key, null, false, true) != null; } public final Spliterator<K> spliterator() { return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0); } }
HashMap 中定義了一個 keySet 的實例屬性,它保存的是整個 HashMap 中全部鍵的集合。上述所列出的 KeySet 類是 Set 的一個實現類,它負責爲咱們提供有關 HashMap 中全部對鍵的操做。
能夠看到,KeySet 中的全部的實例方法都依賴當前的 HashMap 實例,也就是說,咱們對返回的 keySet 集中的任意一個操做都會直接映射到當前 HashMap 實例中,例如你執行刪除一個鍵的操做,那麼 HashMap 中將會少一個節點。
public Collection<V> values() { Collection<V> vs; return (vs = values) == null ? (values = new Values()) : vs; }
values 方法其實和 keySet 方法相似,它返回了全部節點的 value 屬性所構成的 Collection 集合,此處再也不贅述。
public Set<Map.Entry<K,V>> entrySet() { Set<Map.Entry<K,V>> es; return (es = entrySet) == null ? (entrySet = new EntrySet()) : es; }
它返回的是全部節點的集合,或者說是全部的鍵值對集合。