本文的源碼基於jdk8版本,講一下hashMap的核心基本和重難點知識
概述
- hashMap的數據結構是 數組+鏈表+紅黑樹
- 數組查找速度快 鏈表插入和刪除速度比較快
- 鏈表什么時候變成變成紅黑樹
- HashMap就是使用哈希表來存儲的。哈希表為解決沖突,可以采用開放地址法和鏈地址法等來解決問題,Java中HashMap采用了鏈地址法。鏈地址法,簡單來說,就是數組加鏈表的結合
- 節點數大於等於8,並且容量大於64才會把單向鏈表轉換成紅黑樹
- 為了優化查找性能, 鏈表轉變成紅黑樹, 以將 o(n)復雜度的查找效率提升至o(log n)
// 1. 如果鏈表長度大於等於8
if (binCount >= TREEIFY_THRESHOLD - 1) {
treeifyBin(tab, hash);
break;
}
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 2. 如果容量小於64 進行擴容操作
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;
-
我們知道當容量比較小hash的碰撞率也是比較高的,這個代碼中 鏈表長度已經到達8還要去判斷容量大小是否小於64,
如果小於64通過擴容重新給每個key計算和分配到新的數組的位置效率還是比較高的,當容量小於64的時候默認就是16,元素數量會小於 < 16*0.75,數據量非常小,擴容的效率還是非常可觀的, 相對於轉化成紅黑樹會更好,
通過擴容原先碰撞在一起組成鏈表的key會大概率被分散開,因為容量發生變化 hash & (n-1) 的值也會跟着變 -
總結就是當hashMap容量小於64時候,是不會出現紅黑樹的,也就是只有容量大於64,且鏈表長度大於8才會轉換成紅黑樹
一、構造函數
如果我們HashMap在構造時賦一個初始容量,不管你的數值是不是2的次冪,hashMap都會自動將table設為2的整數次冪,接下來我們看看hashMap的構造函數
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
// 默認初始大小 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 默認負載因子 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
final float loadFactor;
/**
* The next size value at which to resize (capacity * load factor).
*
* @serial
*/
// (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;
transient Node<K,V>[] table;
// 沒有指定時, 使用默認值
// 即默認初始大小16, 默認負載因子 0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 指定初始大小, 但使用默認負載因子
// 注意這里其實是調用了另一個構造函數
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);
}
// 利用已經存在的map創建HashMap
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
}
- 我們在構造函數中指定了initialCapacity, 這個值也只被用來計算 threshold
this.threshold = tableSizeFor(initialCapacity);
- tableSizeFor函數干了什么?
/**
* 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;
}
tableSizeFor這個方法用於找到大於等於initialCapacity的最小的2的冪, 這個算法還是很精妙的,
我們都知道, 當一個32位整數不為0時, 32bit中至少有一個位置為1, 上面5個移位操作的目的在於, 將 從最高位的1開始, 一直到最低位的所有bit 全部設為1, 最后再加1(注意, 一開始是先cap-1的), 則得到的數就是大於等於initialCapacity的最小的2的冪
最后我們來看最后一個構造函數, 它調用了 putMapEntries 方法:
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
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);
}
}
}
我們知道, 當使用構造函數HashMap(Map<? extends K, ? extends V> m) 時, 我們並沒有為 table 賦值, 所以, table值一定為null, 我們先根據傳入Map的大小計算 threshold 值, 然后判斷需不需要擴容, 最后調用 putVal方法將傳入的Map插入table中.
- 通過上面對四個構造函數的分析我們發現, 除了最后一個構造函數, 其他三個函數:
HashMap()
HashMap(int initialCapacity)
HashMap(int initialCapacity, float loadFactor)
- 這說明HashMap中, table的初始化或者使用不是在構造函數中進行的, 而是在實際用到的時候, 事實上, 它是在HashMap擴容的時候實現的, 即resize函數(下面會細講)
二、put操作
1. hash函數
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
key的hashCode是一個int類型的數值,長度32位,將hashCode右移16位,然后把int數值的高16位和低16位進行異或
操作(相應位上的數字不相同時,該位才取1,若相同,即為0),這樣可以有效避免高16位不同,但是低16位相同的key的hash碰撞
2. 數組下標計算
i = (n - 1) & hash
&運算
代替%運算
,主要是為了提升運算效率,這樣hashMap的長度必須是2的n次方這樣才能滿足 hash % 2^n = hash & (2^n - 1),這也是hashMap長度必須是2的n次方的原因
3. 操作步驟
-
判斷鍵值對數組table[i]是否為空或為null,否則執行resize()進行擴容;
-
根據鍵值key計算hash值得到插入的數組索引i,如果table[i]==null,直接新建節點添加,轉向⑥,如果table[i]不為空,轉向③;
-
判斷table[i]的首個元素是否和key一樣,如果相同直接覆蓋value,否則轉向④,這里的相同指的是hashCode以及equals;
-
判斷table[i] 是否為treeNode,即table[i] 是否是紅黑樹,如果是紅黑樹,則直接在樹中插入鍵值對,否則轉向⑤;
-
遍歷table[i],判斷鏈表長度是否大於8,大於8的話把鏈表轉換為紅黑樹(還要判斷當前數組容量),在紅黑樹中執行插入操作,否則進行鏈表的插入操作;遍歷過程中若發現key已經存在直接覆蓋value即可;
-
插入成功后,判斷實際存在的鍵值對數量size是否超多了最大容量threshold,如果超過,進行擴容。
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是否是空的
// 我們知道, HashMap的三個構造函數中, 都不會初始Table, 因此第一次put值時, table一定是空的, 需要初始化
// table的初始化用到了resize函數, 這個我們上一篇文章已經講過了
// 由此可見table的初始化是延遲到put操作中的
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 這里利用 `(n-1) & hash` 方法計算 key 所對應的下標
// 如果key所對應的桶里面沒有值, 我們就新建一個Node放入桶里面
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 到這里說明目標位置桶里已經有東西了
else {
Node<K,V> e; K k;
// 這里先判斷當前待存儲的key值和已經存在的key值是否相等
// key值相等必須滿足兩個條件
// 1. hash值相同
// 2. 兩者 `==` 或者 `equals` 等
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p; // key已經存在的情況下, e保存原有的鍵值對
// 到這里說明要保存的桶已經被占用, 且被占用的位置存放的key與待存儲的key值不一致
// 前面已經說過, 當鏈表長度超過8時, 會用紅黑樹存儲, 這里就是判斷存儲桶中放的是鏈表還是紅黑樹
else if (p instanceof TreeNode)
// 紅黑樹的部分以后有機會再說吧
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//到這里說明是鏈表存儲, 我們需要順序遍歷鏈表
else {
for (int binCount = 0; ; ++binCount) {
// 如果已經找到了鏈表的尾節點了,還沒有找到目標key, 則說明目標key不存在,那我們就新建一個節點, 把它接在尾節點的后面
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 如果鏈表的長度達到了8個, 就將鏈表轉換成紅黑數以提升查找性能
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果在鏈表中找到了目標key則直接退出
// 退出時e保存的是目標key的鍵值對
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 到這里說明要么待存儲的key存在, e保存已經存在的值
// 要么待存儲的key不存在, 則已經新建了Node將key值插入, e的值為Null
// 如果待存儲的key值已經存在
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 前面已經解釋過, onlyIfAbsent的意思
// 這里是說舊值存在或者舊值為null的情況下, 用新值覆蓋舊值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); //這個函數只在LinkedHashMap中用到, 這里是空函數
// 返回舊值
return oldValue;
}
}
// 到這里說明table中不存在待存儲的key, 並且我們已經將新的key插入進數組了
++modCount; // 這個暫時用不到
// 因為又插入了新值, 所以我們得把數組大小加1, 並判斷是否需要重新擴容
if (++size > threshold)
resize();
afterNodeInsertion(evict); //這個函數只在LinkedHashMap中用到, 這里是空函數
return null;
}
總結
- 在put之前會檢查table是否為空,說明table真正的初始化並不是發生在構造函數中, 而是發生在第一次put的時候。
- 查找當前key是否存在的條件是p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))
- 如果插入的key值不存在,則值會插入到鏈表的末尾。
- 每次插入操作結束后,都會檢查當前table節點數是否大於threshold, 若超過,則擴容。
- 當鏈表長度超過TREEIFY_THRESHOLD(默認是8)個時,會將鏈表轉換成紅黑樹以提升查找性能。
- 在調用resize方法進行初始化或是擴容操作時,當數組下面的鏈表長度不超過6時,(此時是紅黑書)就會將鏈表由紅黑樹轉為鏈表 UNTREEIFY_THRESHOLD = 6
三、擴容操作 resize
jdk1.8的擴容操作
- 加載因子0.75,為什么?
- 太小hash碰撞就會變高,查詢效率就會降低,太大就會浪費空間,這是在空間和查詢效率上折合出來的
- 什么時候擴容?擴容要進行哪些操作?
- resize發生在table初始化, 或者table中的節點數超過threshold值的時候, threshold的值一般為負載因子乘以容量大小.
- 容量大小為什么是 2的n次冪
- 當只有容量是2的n次冪 才會滿足 hashCode & (length - 1) = hashCode % length
- 每次擴容增加多少?
- 每次擴容的大小是當前容量的2倍
- 每次擴容都會新建一個table, 新建的table的大小為原大小的2倍.
- 擴容時,會將原table中的節點re-hash到新的table中, 但節點在新舊table中的位置存在一定聯系: 要么下標相同, 要么相差一個oldCap(原table的大小)
jdk1.8對擴容做了很多優化,當然源碼內容比較多就不都貼出來,可以自行查看
jdk1.8的resize()方法主要分為兩部分:
- 擴容,擴大數組容量和計算臨界值threshold
- 數據搬家(如果數組數據不為空,需要把數據從原數組搬到擴容后的新數組)
擴容部分這里不細說了,主要說一下數據搬家這部分源碼:
// 下面這段就是把原來table里面的值全部搬到新的table里面
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
// 這里注意, table中存放的只是Node的引用, 這里將oldTab[j]=null只是清除舊表的引用, 但是真正的node節點還在, 只是現在由e指向它
oldTab[j] = null;
// 如果該存儲桶里面只有一個bin, 就直接將它放到新表的目標位置
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;
}
}
}
}
}
數據搬家的時候分為三種情況:數組節點只有一個數據、是紅黑樹、是鏈表
我們都知道jdk1.7的擴容的鏈表操作是有問題的,我們來看看jdk1.8的鏈表數據操作的巧妙之處
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;
}
第一段
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
上面這段定義了四個Node的引用, 從變量命名上,我們初步猜測, 這里定義了兩個鏈表, 我們把它稱為
lo鏈表
和hi鏈表
,loHead
和loTail
分別指向lo鏈表
的頭節點和尾節點,hiHead
和hiTail
以此類推.
第二段
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;
}
上面這段是一個do-while循環, 我們先從中提取出主要框架:
do {
next = e.next;
...
} while ((e = next) != null);
從上面的框架上來看, 就是在按順序遍歷該存儲桶位置上的鏈表中的節點.
我們再看if-else 語句的內容:
// 插入lo鏈表
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
// 插入hi鏈表
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
上面結構類似的兩段看上去就是一個將節點e插入鏈表的動作.
最后再加上 if 塊, 則上面這段的目的就很清晰了:
我們首先准備了兩個鏈表 lo 和 hi, 然后我們順序遍歷該存儲桶上的鏈表的每個節點, 如果 (e.hash & oldCap) == 0, 我們就將節點放入lo鏈表, 否則, 放入hi鏈表.
第三段
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
- 如果lo鏈表非空, 我們就把整個lo鏈表放到新table的j位置上
- 如果hi鏈表非空, 我們就把整個hi鏈表放到新table的j+oldCap位置上
通過上面的剖析我們可以看出,這段代碼的意義就是將原來的鏈表拆分成兩個鏈表, 並將這兩個鏈表分別放到新的table的 j 位置和 j+oldCap 上, j位置就是原鏈表在原table中的位置, 拆分的標准就是:
(e.hash & oldCap) == 0
關於 (e.hash & oldCap) == 0
j 以及 j+oldCap 設計的非常巧妙,省去了重新計算下標的過程,提升了效率,
那我們就有疑惑 為什么原來元素的 hash 和 原數組容量 進行 &
操作就能知道在新數組的位置呢
首先我們要明確三點:
- 數組容量都是 2的整數次冪 (2^n)
- 新數組newCap是原來oldCap的2倍 (2^n+1)
- 下標計算
hash & (n - 1)
其實就是取hash的低m位
我們假設 oldCap = 16
n-1 = 16 -1 = 15
二進制數據就是0000 0000 0000 0000 0000 0000 0000 1111
oldCap = 16
二進制數據就是0000 0000 0000 0000 0000 0000 0001 0000
那 (16-1) & hash
自然就是取hash值的低4位,因為其他位置都是0 我們假設它為 abcd
將oldCap擴大兩倍后, 新的index的位置就變成了 (32-1) & hash, 其實就是取 hash值的低5位
- (32-1)的二進制數據是
0000 0000 0000 0000 0000 0000 0001 1111
一個node的hash值取低5位的情況有兩種
0abcd
1abcd
這里要注意看了
如果hash 值的二進制數據的第4位(起始位置0)為0 那 這個hash & (16-1)
和 hash & (32-1)
計算的下標是一樣的
而且
1abcd = 0abcd + 10000
這里的 0abcd和原來 hash & (16-1) 計算的下標值 10000 正好是 oldCap的二進制的低位
所以 1abcd = 0abcd + 10000 = j + oldCap
得出這樣的結論之后 我們就可以根據 2^n
的第n位是否是1去判斷這個數據的下標,oldCap的正好是第n位有值其他位都是0,
這樣 我們用:
hash & oldCap
- 如果 (e.hash & oldCap) == 0 則該節點在新表的下標位置與舊表一致都為 j
- (e.hash & oldCap) != 0 則該節點在新表的下標位置與舊表一致都為 j+oldCap
如何避免並發問題導致鏈表轉移行程環
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);
- 首先鏈表的操作過程都是局部變量,在鏈表的位置重新計算完成之前 新數組對象不參與,這樣就規避了多線程操作公共變量,只有完成之后賦值給新數組,這樣即便並發只是多賦值幾次不會因為多線程操作造成數據其他影響
- 同樣也沒有像jdk1.7頭插法導致的鏈表轉移后的倒序問題
jdk1.7 擴容並發問題分析
- jdk1.7擴容復制鏈表數據從老數組到新數組過程中使用的時頭插法,這種方式在並發環境下會讓鏈表行程一個環,導致在轉移數據或者get這個hash桶數據時出現死循環
- 就是下面這段代碼導致的這個問題
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
- 為了模擬這個代碼問題 我們寫了個demo
// 首先我們修改一下這個代碼 我們把通過hash計算位置改成了一個默認位置,故意制造hash沖突
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry e : table) {
while (null != e) {
Entry next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
// 指定下標位置
// int i = indexFor(e.hash, newCapacity);
int i = 3;
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
- 然后我們模擬多線程環境下resize過程中調用transfer這個方法
Entry[] table = new Entry[2];
Entry[] newTable = new Entry[5];
/**
* 模擬JDK7 hashMap復制鏈表到新數組邏輯測試
*
* @throws Exception
*/
@Test
public void testTransfer() throws Exception {
Entry entry2 = new Entry(2, "5", "B", null);
Entry entry1 = new Entry(2, "3", "A", entry2);
table[0] = entry1;
CountDownLatch countDownLatch = new CountDownLatch(2);
new Thread(new Runnable() {
@Override
public void run() {
transfer(newTable, false);
countDownLatch.countDown();
}
}, "Thread1").start();
new Thread(new Runnable() {
@Override
public void run() {
transfer(newTable, false);
countDownLatch.countDown();
}
}, "Thread2").start();
countDownLatch.await();
Entry entry = newTable[3];
while (null != entry) {
System.out.println("key:" + entry.key);
entry = entry.next;
}
TimeUnit.HOURS.sleep(1);
}
- 當然我們為了更直觀jdk1.7轉移鏈表邏輯問題,寫死了插入的數組位置,默認插入新數組的下標3的位置
- 無論是jdk1.8的hashMap還是1.7的hashMap 都是線程安全的
五、紅黑樹
紅黑樹特性
- 每個節點要么是紅色,要么是黑色;
- 根節點永遠是黑色的;
- 所有的葉節點都是是黑色的(注意這里說葉子節點其實是上圖中的 NIL 節點,java中的空節點 null);
- 每個紅色節點的兩個子節點一定都是黑色;
- 從任一節點到其子樹中每個葉子節點的路徑都包含相同數量的黑色節點;
紅黑樹的基本增刪查操作,包括求最大最小值,其時間復雜度最壞為O(lgn)
左旋和右旋
- 左旋
x父節點,y子節點,左旋就是把x變成y的左子節點,y成為x父節點
- 右旋
y父節點,x子節點,右旋就是把y變成x的右子節點,x成為y的父節點
紅黑樹和平衡二叉樹對比
紅黑樹的查詢性能略微遜色於AVL樹,因為他比avl樹會稍微不平衡最多一層,也就是說紅黑樹的查詢性能只比相同內容的avl樹最多多一次比較,但是,紅黑樹在插入和刪除上完爆avl樹,avl樹每次插入刪除會進行大量的平衡度計算,而紅黑樹為了維持紅黑性質所做的紅黑變換和旋轉的開銷,相較於avl樹為了維持平衡的開銷要小得多
總結
- HashMap:它根據鍵的hashCode值存儲數據,大多數情況下可以直接定位到它的值,因而具有很快的訪問速度,但遍歷順序卻是不確定的。 HashMap最多只允許一條記錄的鍵為null,允許多條記錄的值為null。HashMap非線程安全,即任一時刻可以有多個線程同時寫HashMap,可能會導致數據的不一致。如果需要滿足線程安全,可以用 Collections的synchronizedMap方法使HashMap具有線程安全的能力,或者使用ConcurrentHashMap
Collections.synchronizedMap(hashMap).put("hello","瘋狂造輪子");
- hashMap賦值初始化大小,如果能夠估算要添加的數據量 賦值
initialCapacity
可以大大減少擴容帶來的巨大的性能消耗 - hashMap允許一個key是null,允許多個值為null