HashMap的曆史
HashMap最早是在jdk1.2中開始出現的,一直到jdk1.7一直沒有太大的變化。
但是到了jdk1.8突然進行了一個很大的改動。其中一個最顯著的改動就是:
之前jdk1.7的存儲結構是數組+連結清單,到了jdk1.8變成了數組+連結清單+紅黑樹。
另外,HashMap是非線程安全的,進行增删改操作的時候,是不能保證資料的一緻性的。

紅黑樹是一個自平衡的二叉查找樹,也就是說紅黑樹的查找效率是非常的高,查找效率會從連結清單的o(n)降低為o(logn)。
HashMap名詞介紹
名稱 | 用途 |
---|---|
initialCapacity | HashMap 初始容量 |
loadFactor | 負載因子 |
threshold | 目前 HashMap 所能容納鍵值對數量的最大值,超過這個值,則需擴容 |
負載因子(loadFactor)
對于 HashMap 來說,負載因子是一個很重要的參數,該參數反應了 HashMap 桶數組的使用情況。
通過調節負載因子,可使 HashMap 時間和空間複雜度上有不同的表現。
當我們調低負載因子時,HashMap 所能容納的鍵值對數量變少。
擴容時,重新将鍵值對存儲新的桶數組裡,鍵的鍵之間産生的碰撞會下降,連結清單長度變短。此時,HashMap 的增删改查等操作的效率将會變高,這裡是典型的拿空間換時間。
相反,如果增加負載因子(負載因子可以大于1),HashMap 所能容納的鍵值對數量變多,空間使用率高,但碰撞率也高。這意味着連結清單長度變長,效率也随之降低,這種情況是拿時間換空間。
至于負載因子怎麼調節,這個看使用場景了。一般情況下,我們用預設值就可以了。
Node[] table
即哈希桶數組 ,Node是HashMap的一個内部類,實作了Map.Entry接口,本質是就是一個映射(鍵值對)。
Node[] table的初始化長度length(預設值是16),
Load factor為負載因子(預設值是0.75),
threshold是HashMap所能容納的最大資料量的Node(鍵值對)個數。
threshold = length * Load factor。
也就是說,在數組定義好長度之後,負載因子越大,所能容納的鍵值對個數越多。
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; } }
源碼解析
初始化 threshold 的方法
/** * Returns a power of two size for the given target capacity. */ 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 的最小2的幂。
put(K key, V value)
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
也就是說,put方法其實調用的是putVal方法。putVal方法有5個參數: (1)第一個參數hash:調用了hash方法計算hash值 (2)第二個參數key:就是我們傳入的key值,也就是例子中的張三 (3)第三個參數value:就是我們傳入的value值,也就是例子中的20 (4)第四個參數onlyIfAbsent:也就是當鍵相同時,不修改已存在的值 (5)第五個參數evict :如果為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尚未初始化,則此處進行初始化數組,并指派初始容量,重新計算門檻值 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //第二部分 //判斷hash值,不沖突,new一個Node插入進去 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //第三部分 //處理沖突,如果通過hash找到的位置有資料,發生碰撞 else { Node<K,V> e; K k; //第三部分--A //p是目前節點,如果需要插入的key和目前hash值指定下标的key一樣,一樣的直接替換舊值e if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //第三部分--B //如果此時桶中資料類型為 treeNode,使用紅黑樹進行插入 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //第三部分--C //此時桶中資料類型為連結清單 else { //看這裡的 .next,如果是數組,就會進去周遊 //沒有 就newNode //化樹的門檻值,TREEIFY_THRESHOLD,是以不存在并且在連結清單末尾插入元素的時候 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; } //如果連結清單中有新插入的節點位置資料不為空,則此時e 指派為節點的值,跳出循環 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //經過上面的循環後,如果e不為空,則說明上面插入的值已經存在于目前的hashMap中, //那麼更新指定位置的鍵值對 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } //第四部分,判斷是否大于門檻值,是否擴容 ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
代碼看,put方法分為三種情況:
- table尚未初始化,對資料進行初始化
- table已經初始化,且通過hash算法找到下标所在的位置資料為空,直接将資料存放到指定位置
- table已經初始化,且通過hash算法找到下标所在的位置資料不為空,發生hash沖突(碰撞),發生碰撞後,會執行以下操作:
- 判斷插入的key如果等于目前位置的key的話,将 e 指向該鍵值對
- 如果此時桶中資料類型為 treeNode,使用紅黑樹進行插入
- 如果是連結清單,則進行循環判斷, 如果連結清單中包含該節點,跳出循環,如果連結清單中不包含該節點,則把該節點插入到連結清單末尾,同時,如果連結清單長度超過樹化門檻值(TREEIFY_THRESHOLD)且table容量超過最小樹化容量(MIN_TREEIFY_CAPACITY),則進行連結清單轉紅黑樹(由于table容量越小,越容易發生hash沖突,是以在table容量<MIN_TREEIFY_CAPACITY 的時候,如果連結清單長度>TREEIFY_THRESHOLD,會優先選擇擴容,否則會進行連結清單轉紅黑樹操作)
擴容前的小知識
怎麼計算Hash值?
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
與16異或的位運算。
在資料結構中,我們處理hash沖突常使用的方法有:開發定址法、再哈希法、鍊位址法、建立公共溢出區。而hashMap中處理hash沖突的方法就是鍊位址法。
“鍊”,很容易想到,因為常用删除修改等操作,是以效率更高。
initialCapacity初始容量(你輸入的最接近2的N次幂的數字),loadFactor負載因子(0.75,泊松分布)。
/* ---------------- Public operations -------------- */ /** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and load factor. * * @param initialCapacity the initial capacity * @param loadFactor the load factor * @throws IllegalArgumentException if the initial capacity is negative * or the load factor is nonpositive */ 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); } /** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and the default load factor (0.75). * * @param initialCapacity the initial capacity. * @throws IllegalArgumentException if the initial capacity is negative. */ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } /** * Constructs an empty <tt>HashMap</tt> with the default initial capacity * (16) and the default load factor (0.75). */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } /** * Constructs a new <tt>HashMap</tt> with the same mappings as the * specified <tt>Map</tt>. The <tt>HashMap</tt> is created with * default load factor (0.75) and an initial capacity sufficient to * hold the mappings in the specified <tt>Map</tt>. * * @param m the map whose mappings are to be placed in this map * @throws NullPointerException if the specified map is null */ public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
擴容 resize()
/** * Initializes or doubles table size. If null, allocates in * accord with initial capacity target held in field threshold. * Otherwise, because we are using power-of-two expansion, the * elements from each bin must either stay at same index, or move * with a power of two offset in the new table. * * @return the table */ //初始化或加倍表大小。如果為空,則根據字段門檻值中持有的初始容量目标進行配置設定。否則,因為我們使用的是 2 的幂擴充,是以每個 bin 中的元素必須保持相同的索引,或者在新表中以 2 的幂的偏移量移動。 final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; //threshold 臨界點 int newCap, newThr = 0; //第一部分,擴容 //如果超過了直接将門檻值設定為整數的最大值,沒有,則,通過移位操作擴大2倍。 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; //threshold 和 table 皆未初始化情況,此處即為首次進行初始化 //也就在此處解釋了構造方法中沒有對threshold 和 初始容量進行指派的問題 else { // zero initial threshold signifies using defaults //如果門檻值為零,表示使用預設的初始化值 //這種情況在調用無參構造的時候會出現,此時使用預設的容量和門檻值 newCap = DEFAULT_INITIAL_CAPACITY; //此處門檻值即為 threshold=initialCapacity*loadFactor newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 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; //第三部分 //将舊的資料遷移到新的數組 //如果之前的數組桶裡面已經存在資料,由于table容量發生變化,hash值也會發生變化,需要重新計算下标 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) //直接将資料存放到新計算的hash值下标下 newTab[e.hash & (newCap - 1)] = e; //如果是TreeNode資料結構 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //對于連結清單,資料結構 else { // preserve order //如果是連結清單,重新計算hash值,根據新的下标重新分組 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; }
resize分為以下幾步:
- 首先先判斷目前table是否進行過初始化,如果沒有進行過初始化,此處就解決了調用無參構造方法時候,threshold和initialCapacity 未初始化的問題,如果已經初始化過了,則進行擴容,容量為原來的二倍
- 擴容後建立新的table,并對所有的資料進行周遊
- 如果新計算的位置資料為空,則直接插入
- 如果新計算的位置為連結清單,則通過hash算法重新計算下标,對連結清單進行分組
- 如果是紅黑樹,則需要進行拆分操作
get方法,查找
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) { //根據hash算法找到對應位置的第一個資料,如果是指定的key,則直接傳回 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源碼
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
這段代碼叫做擾動函數,也是hashMap中的hash運算,主要分為下面幾步:
- key.hashCode(),擷取key的hashCode值,如果不進行重寫的話傳回的是根據記憶體位址得到的一個int值
- key.hashCode() 擷取到的hashcode無符号右移16位并和元hashCode進行^ ,這樣做的目的是為了讓高位與低進行混合,讓兩者都參與運算,以便讓hash值分布更加均勻
取模運算 (n - 1) & hash
在hashMap的代碼中,在很多地方都會看到類似的代碼:
first = tab[(n - 1) & hash])
hash算法中,為了使元素分布的更加均勻,很多都會使用取模運算,
在hashMap中并沒有使用hash%n這樣進行取模運算,而是使用(n - 1) & hash進行代替,
原因是在計算機中,&的效率要遠高于%。
需要注意的是, 隻有容量為2的n次幂的時候,(n - 1) & hash 才能等效hash%n, 這也是hashMap 初始化初始容量時,無論傳入任何值,都會通過tableSizeFor(int cap) 方法轉化成2的n次幂的原因, 這種巧妙的設計真的很令人驚歎。
有興趣的可以看一下一位大佬寫的文章:由HashMap雜湊演算法引出的求餘%和與運算&轉換問題
remove方法,删除
public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } 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; //根據key和key的hash值,查找到對應的元素 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); } } //如果查找的了元素node,移除即可 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { //如果是TreeNode,通過樹進行移除 if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); //如果是第一個節點,移除第一個節點,将index下标的位置指向第二個節點 else if (node == p) tab[index] = node.next; else //如果不是連結清單的第一個節點,則移除該節點 p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }
連結清單樹化
static final int TREEIFY_THRESHOLD = 8; /** * 當桶數組容量小于該值時,優先進行擴容,而不是樹化 */ static final int MIN_TREEIFY_CAPACITY = 64; 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); } } /** * 将普通節點連結清單轉換成樹形節點連結清單 */ final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; // 桶數組容量小于 MIN_TREEIFY_CAPACITY,優先進行擴容而不是樹化 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { // hd 為頭節點(head),tl 為尾節點(tail) 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<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) { return new TreeNode<>(p.hash, p.key, p.value, next); }
在擴容過程中,樹化要滿足兩個條件:
- 連結清單長度大于等于 TREEIFY_THRESHOLD
- 桶數組容量大于等于 MIN_TREEIFY_CAPACITY
紅黑樹鍊化與拆分
鍊化
final Node<K,V> untreeify(HashMap<K,V> map) { Node<K,V> hd = null, tl = null; // 周遊 TreeNode 連結清單,并用 Node 替換 for (Node<K,V> q = this; q != null; q = q.next) { // 替換節點類型 Node<K,V> p = map.replacementNode(q, null); if (tl == null) hd = p; else tl.next = p; tl = p; } return hd; } Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) { return new Node<>(p.hash, p.key, p.value, next); }
拆分
// 紅黑樹轉連結清單門檻值 static final int UNTREEIFY_THRESHOLD = 6; final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) { TreeNode<K,V> b = this; // Relink into lo and hi lists, preserving order TreeNode<K,V> loHead = null, loTail = null; TreeNode<K,V> hiHead = null, hiTail = null; int lc = 0, hc = 0; /* * 紅黑樹節點仍然保留了 next 引用,故仍可以按連結清單方式周遊紅黑樹。 * 下面的循環是對紅黑樹節點進行分組,與上面類似 */ for (TreeNode<K,V> e = b, next; e != null; e = next) { next = (TreeNode<K,V>)e.next; e.next = null; if ((e.hash & bit) == 0) { if ((e.prev = loTail) == null) loHead = e; else loTail.next = e; loTail = e; ++lc; } else { if ((e.prev = hiTail) == null) hiHead = e; else hiTail.next = e; hiTail = e; ++hc; } } if (loHead != null) { // 如果 loHead 不為空,且連結清單長度小于等于 6,則将紅黑樹轉成連結清單 if (lc <= UNTREEIFY_THRESHOLD) tab[index] = loHead.untreeify(map); else { tab[index] = loHead; /* * hiHead == null 時,表明擴容後, * 所有節點仍在原位置,樹結構不變,無需重新樹化 */ if (hiHead != null) loHead.treeify(tab); } } // 與上面類似 if (hiHead != null) { if (hc <= UNTREEIFY_THRESHOLD) tab[index + bit] = hiHead.untreeify(map); else { tab[index + bit] = hiHead; if (loHead != null) hiHead.treeify(tab); } } }
在将普通連結清單轉成紅黑樹時,HashMap 通過兩個額外的引用 next 和 prev 保留了原連結清單的節點順序。
這樣再對紅黑樹進行重新映射時,完全可以按照映射連結清單的方式進行。這樣就避免了将紅黑樹轉成連結清單後再進行映射,無形中提高了效率。
從源碼上可以看得出,重新映射紅黑樹的邏輯和重新映射連結清單的邏輯基本一緻。不同的地方在于,重新映射後,會将紅黑樹拆分成兩條由 TreeNode 組成的連結清單。
如果連結清單長度小于 UNTREEIFY_THRESHOLD,則将連結清單轉換成普通連結清單。否則根據條件重新将 TreeNode 連結清單樹化。
被 transient 所修飾 table 變量
其實這個關鍵字的作用很好了解,
就是簡單的一句話:将不需要序列化的屬性前添加關鍵字transient,序列化對象的時候,這個屬性就不會被序列化。
transient Node<K,V>[] table; /** * Holds cached entrySet(). Note that AbstractMap fields are used * for keySet() and values(). */ transient Set<Map.Entry<K,V>> entrySet; /** * The number of key-value mappings contained in this map. */ transient int size; /** * The number of times this HashMap has been structurally modified * Structural modifications are those that change the number of mappings in * the HashMap or otherwise modify its internal structure (e.g., * rehash). This field is used to make iterators on Collection-views of * the HashMap fail-fast. (See ConcurrentModificationException). */ transient int modCount;
HashMap 并沒有使用預設的序列化機制,而是通過實作
readObject/writeObject
兩個方法自定義了序列化的内容。
序列化 talbe 存在着兩個問題:
- table 多數情況下是無法被存滿的,序列化未使用的部分,浪費空間
- 同一個鍵值對在不同 JVM 下,所處的桶位置可能是不同的,在不同的 JVM 下反序列化 table 可能會發生錯誤。
第二個問題解釋:HashMap 的
get/put/remove
等方法第一步就是根據 hash 找到鍵所在的桶位置,
但如果鍵沒有覆寫 hashCode 方法,計算 hash 時最終調用 Object 中的 hashCode 方法。但 Object 中的 hashCode 方法是 native 型的,不同的 JVM 下,可能會有不同的實作,産生的 hash 可能也是不一樣的。也就是說同一個鍵在不同平台下可能會産生不同的 hash,此時再對在同一個 table 繼續操作,就會出現問題。
總結
- HashMap 底層資料結構在JDK1.7之前是由數組+連結清單組成的,1.8之後又加入了紅黑樹;連結清單長度小于8的時候,發生Hash沖突後會增加連結清單的長度,當連結清單長度大于8的時候,會先判讀數組的容量,如果容量小于64會先擴容(原因是數組容量越小,越容易發生碰撞,是以當容量過小的時候,首先要考慮的是擴容),如果容量大于64,則會将連結清單轉化成紅黑樹以提升效率。
- hashMap 的容量是2的n次幂,無論在初始化的時候傳入的初始容量是多少,最終都會轉化成2的n次幂,這樣做的原因是為了在取模運算的時候可以使用&運算符,而不是%取餘,可以極大的提上效率,同時也降低hash沖突。
- HashMap是非線程安全的,在多線程的操作下會存在異常情況(如形成閉環(1.7),1.8已修複閉環問題,但仍不安全),可以使用HashTable或者ConcurrentHashMap進行代替
HashMap這樣的知識點還有很多,一篇文章是說不完的,至于更多的玄機需要我們一起去探索。
參考連結:
HashMap源碼分析(jdk1.8,保證你能看懂) - 知乎 (zhihu.com)
Java 8系列之重新認識HashMap - 知乎 (zhihu.com)
有大量時間看這個,特别全: HashMap 源碼詳細分析(JDK1.8) - SegmentFault 思否
HashMap源碼分析 —— 一篇文章搞定HashMap面試 (juejin.cn)