HashMap是基於Map接口的一個非同步實現,此實現提供key-value形式的數據映射,支持null值。node
HashMap的常量和重要變量以下:算法
DEFAULT_INITIAL_CAPACITY = 16
|
Node數組的默認長度
|
MAXIMUM_CAPACITY = 1073741824
|
Node數組的最大長度
|
DEFAULT_LOAD_FACTOR = 0.75F
|
負載因子,調控控件與衝突率的因數
|
TREEIFY_THRESHOLD = 8
|
鏈表轉換爲樹的閾值,超過這個長度的鏈表會被轉換爲紅黑樹
|
UNTREEIFY_THRESHOLD = 6
|
當進行resize操做時,小於這個長度的樹會被轉換爲鏈表
|
MIN_TREEIFY_CAPACITY = 64
|
鏈表被轉換成樹形的最小容量,若是沒有達到這個容量只會執行resize進行擴容
|
Node<K, V>[] table
|
儲存元素的數組
|
Set<Map.Entry<K, V>> entrySet
|
set數組,用於迭代元素
|
int size
|
存放元素的個數,但不等於數組的長度
|
int modCount
|
每次擴容和更改map結構的計數器
|
int threshold
|
臨界值,當實際大小(容量*負載因子)超過臨界值的時候,會進行擴容
|
float loadFactor
|
負載因子,默認爲0.75F
|
在jdk1.8中,HashMap是採用數組+鏈表+紅黑樹的形式實現。以下圖: 數組
其中鏈表的實現以下:安全
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中包含一個next變量,這個就是鏈表的關鍵點,hash結果相同的元素就是經過這個next進行關聯的。性能
接下來看看紅黑樹的實現:this
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); }
......
}
紅黑樹比鏈表多了四個變量,parent父節點、left左節點、right右節點、prev上一個同級節點,紅黑樹內容較多,有興趣的能夠自行百度,不在贅述。spa
在來講說hash算法,HashMap中使用的算法以下線程
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
這個hash先將key右移了16位,而後與key進行異或,這裏還涉及到後面put方法中的另外一次&操做,code
tab[i = (n - 1) & hash]
tab既是table,n是map集合的容量大小,hash是上面方法的返回值。由於一般聲明map集合時不會指定大小,或者初始化的時候就建立一個容量很大的map對象,因此這個經過容量大小與key值進行hash的算法在開始的時候只會對低位進行計算,雖然容量的2進制高位一開始都是0,可是key的2進制高位一般是有值的,所以先在hash方法中將key的hashCode右移16位在與自身異或,使得高位也能夠參與hash,更大程度上減小了碰撞率。對象
構造方法以下:
public HashMap() //默認構造方法 public HashMap(int initialCapacity)//參數爲初始大小 public HashMap(int initialCapacity, float loadFactor)//參數爲初始大小,負載因子
這裏又涉及到另外一個算法:
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; }
這個算法頗有意思,經過給定的大小cap,計算大於等於cap的最小的2的冪數。連續5次右移運算乍一看沒有什麼意思,但仔細一想2進制都是0和1啊,這就有問題了,第一次右移一位,就表示但凡是1的位置右邊的一位都變成了1,第二次右移兩位,上次已經把有1的位置都變成連續兩個1了,是否是感受很神奇,如此下來5次運算正好將int的32位都轉了個遍,以最高的一個1的位置爲基準將後面全部位數都變爲1,而後在進行n+1,不就變成了2的冪數。這裏還有一點要注意的是第一行的cap-1,這是由於若是cap自己就是2的冪數,會出現結果是cap的2倍的狀況,會浪費空間。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //put方法的實際執行者
//hash,key的hash值;onlyIfAbsent,是否改變原有值;evict,LinkedHashMap回調時纔會用到。
Node<K,V>[] tab;
Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //table爲空或長度爲0時,對table進行初始化,分配內存 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //當put的key在map中不存在時,直接new一個Node存入table。 else { //當key在map中存在時,進入此分支。 Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //此處的p是table中的第n-1個元素,每一次必檢查此元素的key是否與put的key相同,若是相同則替換value e = p; else if (p instanceof TreeNode) //檢查p是不是TreeNode類型。 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //若是第n-1個元素不符合,則一次遍歷table中其他元素,直到找到相應key值。 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { //此處主要爲了防止hash出現重複,只有上邊tab[i = (n - 1) & hash]中存在元素且不是put的元素時纔會進入這個分支。 p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // 此處判斷的意義是當鏈表長度超過8時,轉換爲紅黑樹,在1.8之前是沒有的,鏈表查詢的複雜度是O(n),而紅黑樹是O(log(n)),可是若是hash結果不均勻會極大的影響性能 treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) //進入此分支標誌已找到與put的key值相同的元素,元素變量爲e。 break; p = e; } } if (e != null) { // 將對應key的value替換爲put的value,同時返回舊value,只有存在key時纔會返回! V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); //對table進行擴容 afterNodeInsertion(evict); return null; }
從上面的源碼能夠看出,首先會判斷key的hash與map容量-1的與計算值,若是數組中這個位置沒有元素則直接插入,反之則在遍歷此位置的元素鏈表,直到在最後插入。這個地方有一個鏈表的閾值(默認是8),若是一個鏈表的長度達到了閾值,則調用treefyBin()將鏈表轉換成紅黑樹。
final Node<K,V>[] resize() { //擴容方法
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length; //map如今的容量
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; // 若是如今的容量擴大爲2倍依然沒有超過最大容量,而且如今的容量大於等於數組的默認長度,將map的容量擴大爲2倍 } else if (oldThr > 0) //hashMap有一個構造方法會指定初始大小,這時候就會用到這個分支,對map進行初始化 newCap = oldThr; else { //這個分支是默認的初始化參數分支,好比用new hashMap()生成一個對象,就會進入這個分支初始化 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) { //擴容操做,將oldTab的元素依次添加到newTab 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; }
在這個方法裏有幾個比較有意思的地方,首先是最大容量MAXIMUN_CAPACITY,這個值實際就是int的最大值,我的推測應該是爲了配合put時的hash算法,由於計算key值hash的算法返回值是int型,若是容量超過int的閾值,在進行與運算時碰撞率會增大不少倍。而後是擴容的方式,這裏是new了一個數組!這也是HashMap不安全的關鍵之一。當超過一個線程對一個HashMap對象進行put的時候若是觸發了resize方法,後執行的那一個會把以前執行的結果覆蓋掉!也就是先put的值不會真的存入map中。
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) //若是map的容量小於64(默認值),會調用resize擴容,不會轉換爲紅黑樹 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); //Node轉換爲TreeNode 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的樹排序方法 } }
這裏須要注意的是當map容量小於64時,就算鏈表超過了8也不會轉換爲紅黑樹。
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { //主要用於clone方法和putAll方法和一個構造方法HashMap(Map<? extends K, ? extends V> m)。 int s = m.size(); if (s > 0) { if (table == null) { // 若是是一個new的map,即table變量尚未初始化,先經過參數的map.size和負載因子計算所需的map大小。 float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); if (t > threshold) //若是計算出的大小大於map的實際存儲容量,則從新計算存儲容量 threshold = tableSizeFor(t); } else if (s > threshold) //若是參數map大小大於實際存儲容量,則將map容量變爲2倍 resize(); for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { //遍歷賦值 K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } }
final Node<K,V> getNode(int hash, Object key) { //get的實際實現方法
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) { //經過hash定位到一個桶,而且有元素存在 if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) //老是檢查桶中的第一個元素 return first; if ((e = first.next) != null) { //若是第一個元素不符合,則繼續搜索 if (first instanceof TreeNode) //若是桶中是紅黑樹,進入此分支,實際執行的是TreeNode中的find方法,從根節點開始向下搜尋 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; }
看過put方法後再來看get就容易不少了,首先用put中的hash算法定位到數組中的位置,若是有元素且key值相同直接返回,若是有元素且key值不一樣那就要向下遍歷了,若是是鏈表就一直遍歷next,若是是紅黑樹則調用getTreeNode方法查找節點。
abstract class HashIterator { //KeySet和EntrySet的迭代器的父類 Node<K,V> next; // 下一個元素 Node<K,V> current; // 刪除方法會調用 int expectedModCount; // 就是hashMap中的modCount int index; // 元素序號 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); } } ....... }
HashIterator是KerSet和EntrySet的父類,這個迭代器中有一個屬性是expectedModCount要特別注意,這個變量等價於HashMap對象中的modCount,若是在迭代器沒有執行完的期間對map進行增刪,會拋出異常阻止繼續迭代,代碼以下:
final Node<K,V> nextNode() { //獲取下一個元素 Node<K,V>[] t; Node<K,V> e = next; if (modCount != expectedModCount) //在迭代器執行期間只能用下面的remove刪除元素,不然會拋出異常,不能增長元素 throw new ConcurrentModificationException(); if (e == null) //若是沒有下一個會拋出異常 throw new NoSuchElementException(); if ((next = (current = e).next) == null && (t = table) != null) { //若是next沒有取到值,經過index繼續向下遍歷,直到取到元素爲止 do {} while (index < t.length && (next = t[index++]) == null); } return e; }
不過迭代器雖然不容許添加元素,可是給了一個刪除的方法:
public final void remove() { //刪除方法 Node<K,V> p = current; if (p == null) throw new IllegalStateException(); if (modCount != expectedModCount) throw new ConcurrentModificationException(); current = null; K key = p.key; removeNode(hash(key), key, null, false, false); expectedModCount = modCount; //關鍵語句,這裏作了一個同步,不然與正常的刪除沒有區別,這個同步避免了上面的異常拋出 }
與普通的刪除方法就差在最後這一行上,這裏同步了迭代器和HashMap對象的操做數,便不會拋出異常