集合:java
ArrayList 數組node
LinkedList 鏈表數組
private static class Node<E> { E item; Node<E> next; Node<E> prev;
猜測HashMap底層的數據結構安全
數據結構:數據結構
集合、線性結構、樹形結構、圖多線程
那麼由上面的數據結構猜測HashMap的結構是Key和value組成,多是數組和鏈表組成app
那麼HashMap存儲的單元是函數
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; // key V value; // value Node<K,V> next; //下一個
數組的表示怎麼表示呢?源碼分析
String[]、Integer[]this
transient Node<K,V>[] table;
那麼數組的大小呢
/** * The default initial capacity - MUST be a power of two. */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
初始化大小爲16,爲何使用位運算?1.快,2.MUST be a power of two.
若是16的初始值用完了,怎麼辦?
那麼須要擴容,擴容的條件是使用量達到了百分之75.
/** * The load factor used when none specified in constructor. */ static final float DEFAULT_LOAD_FACTOR = 0.75f;
使用一個變量去記錄這個數組使用了多少了
/** * The number of key-value mappings contained in this map. */ transient int size;
// (The javadoc description is true upon serialization. // Additionally, if the table array has not been allocated, this // field holds the initial array capacity, or zero signifying // DEFAULT_INITIAL_CAPACITY.) int threshold; //閾值,也就是數組長度乘以0.75
鏈表的長度也要有限制
/** * The bin count threshold for using a tree rather than list for a * bin. Bins are converted to trees when adding an element to a * bin with at least this many nodes. The value must be greater * than 2 and should be at least 8 to mesh with assumptions in * tree removal about conversion back to plain bins upon * shrinkage. */ static final int TREEIFY_THRESHOLD = 8;
若是鏈表長度超過了8就採用紅黑樹(平衡二叉樹)來存儲(jdk1.8)
以上是基於已有的知識猜測的,下面就去源碼驗證
經常使用的put方法
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
裏面的Hash函數,是一個Object自帶的方法獲取到的是一個整形,可是這個hashcode容易重複,會致使不一樣的元素存儲到同一個數組下標位置,還有一個問題,咱們的數組只有16
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); // hash值是32位, }
hash函數就是將高16爲和低16爲進行異或運算,獲得的一個結果。
是爲了讓node落點分佈均勻,減小碰撞的機率,若是碰撞高了,就會致使數組下標的鏈表很長
a. 0000000000000000 0000000000000000 32位的hashcode
b. 0000000000000111 1110000000000000
c. 0000000000000000 1110000000000000
若是不作低16位和高16位的與運算(hashcode就是一個32位的int型),那麼b和c將會落到同一個節點上,也就是說若是全部低16位相同的都會落到相同的節點,這樣顯然不合適,會致使單個節點的鏈表變得很長。這裏可能會疑惑若是個人數組不在是16而是一個很大的數字,那麼高16位不就是 能夠參與運算了嗎?確實是這樣的,可是源碼中是這樣定義了數組的最大值
/** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. */ static final int MAXIMUM_CAPACITY = 1 << 30;
也就是說不可能超過32位,還有個緣由是,咱們使用hashmap時大部分時間也不會存儲那麼大的 數據,通常也就是低幾位的數字就搞定了。
如今的問題是hash函數的值太大,咱們的數組並無那麼大,怎樣讓他落到咱們的數組裏面呢?
1.使用Hash%16取末,必定不會超過16
2. n表示數組長度,tab表示數組,hash表示Hash值
(p = tab[i = (n - 1) & hash])
0111110111 & 001111 ----》15到0
這裏就能看出MUST be a power of two.的意圖了,由於若是不是2的n次冪,那麼就會致使數據不能均勻分佈到數組上。
例如
a. 0111110011 &
b. 0111110111 &
01011 不是2的n次冪,從右到左的第三位爲0了,無論a、b的第三位爲何,進行&計算後都是0,就會致使不均勻,由於2的n次冪是標準,不能有偏向性。
初始化數組
n = (tab = resize()).length;
這裏的resize()方法作數組初始化
// zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; //16 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //12
使用默認的初始值(16,12)初始化數組
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab;
開始放入數據,判斷放入數據點的位置是否爲空
if ((p = tab[i = (n - 1) & hash]) == null)
爲空就直接放進去
tab[i] = newNode(hash, key, value, null);
不爲空,就有三種狀況
1.key存在了,就覆蓋,並返回舊值
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; }
2.是鏈表,由代碼能夠看出,新的數據是添加到鏈表的末尾的(jdk1.8)
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); //超過了8就變紅黑樹 break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; }
3.是紅黑樹
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
判斷數據是否超過了閾值
if (++size > threshold) // threshold=12 resize(); // 擴容
因此由上面能夠看出resize()既能夠初始化大小也能夠擴容。
如何擴容的?
1.擴展空間
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 // 閾值也擴大一倍 }
2.把舊數據均勻分佈在新的空間上
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; // 原位置設置爲null if (e.next == null) // 沒有子節點 newTab[e.hash & (newCap - 1)] = e; // 使用hashcode & n-1 計算新的位置 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; } } } } }
(e.hash & oldCap) == 0) // 省去了e.hash & n-1
16 10000 第五位確定爲1
110010101100111 &
?0000 若是第五位是0,那麼上面的第五位必定是0
011111 這是新的32位的數組,作&運算後,第五位必定是0,那麼這個元素在16和32大小的數組裏面都是落在相同位置。
就像這樣一個特殊數字
a. 110010101101111 &
b. 01111 15
c. 10000 16
d. 011111 32
a&b == a&d = 1111 也就說明這個節點是不須要移動的
得出新的節點的Hash的值,第五位是0,這樣就能判斷出16位的和32位的與運算出來的結果是同樣的,就不須要移動數據。
若是不等於0就須要移動
if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead;// 將該數據移動到本身如今的位置+就數組的長度的位置處 }
這裏補充一句,因爲jdk1.7和jdk1.8的實現不少不一樣,在jdk7中HashMap在多線程環境下出現的死循環已經被上面介紹的擴容機制規避了。 若是是多線程環境仍是應該使用HashTable、ConcurrentHashMap、Synchronized Map這些線程安全的類。