目錄
3.1、DEFAULT_INITIAL_CAPACITY(默認桶數組大小)
3.2、DEFAULT_LOAD_FACTOR(默認負載因子大小)
3.4、MIN_TREEIFY_CAPACITY(哈希表的最小樹形化容量)
3.5、TREEIFY_THRESHOLD(一個桶的樹化閾值)
3.6、UNTREEIFY_THRESHOLD(一個樹的鏈表還原閾值)
5.1、hash() 和【(n-1)& hash】 - hash相關
5.7、modCount 和 ConcurrentModificationException
6.2、HashMapSpliterator(內部類分割迭代器 )
8.2、ConcurrentHashMap CAS 和 分段鎖
9.2、JDK8 ConcurrentHashMap CPU 100%
1、前言
在開始之前我們需要思考一件事情,大部分人在關注HashMap會把核心重點放在裏面的紅黑樹,或者樹相關的算法上面,紅黑樹固然很高效也很有效果,但是是否有人真正思考過,紅黑樹是否位HashMap的核心呢?HashMap高效是否真的體現在紅黑樹上面呢?在JDK1.8之前,HashMap沒有使用紅黑樹,那麼它又是怎麼實現高效的能?還有紅黑樹真的是用來提高查詢效率的嗎?換句話說,紅黑樹的插入查詢效率真的比Hash的走類似於索引的方式更加快嗎?本次文章將會針對HashMap中的各種常量,各種方法來說明HashMap爲什麼高效,以及爲什麼HashMap要使用紅黑樹,解讀負載因子等內容,同時來滿足大家的好奇心。同時本次閱讀源碼的時候,希望大家能多閱讀代碼上方的註釋,很多時候精華就在註釋上面,在開始之前希望大家能先去了解一下位運算,比如左移和右移,邏輯右移和算術右移等等。
在開始前我們首先介紹一下HashMap的幾個作者:
Doug Lea (Java併發之父)、Josh Bloch 、Arthur van Hoff、Neal Gafter
我們可以簡單看一下HashMap類的依賴關係
從圖中可以看出ConcurrentHashMap 和 HashMap都是單獨實現的,並不存在繼承光系,或者可以這麼說ConcurrentHashMap相當於用併發手段重新實現了一次HashMap
基本上整個HashMap的結構是這樣的
HashMap核心數據結構-Node數組(大小一定是2的冪)
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table;
時間複雜度
紅黑樹查詢:O(log(N))
鏈表查詢:O(n)
2、什麼是Hash
Hash,一般翻譯做“散列”,也有直接音譯爲“哈希”的,就是把任意長度的輸入(又叫做預映射pre-image)通過散列算法變換成固定長度的輸出,該輸出就是散列值。這種轉換是一種壓縮映射,也就是,散列值的空間通常遠小於輸入的空間,不同的輸入可能會散列成相同的輸出,所以不可能從散列值來確定唯一的輸入值。簡單的說就是一種將任意長度的消息壓縮到某一固定長度的消息摘要的函數(來源百度百科)
換句話說,就是把一堆數據,變成一段定長的映射,這段定長由程序來控制決定
3、常量
3.1、DEFAULT_INITIAL_CAPACITY(默認桶數組大小)
3.2、DEFAULT_LOAD_FACTOR(默認負載因子大小)
3.3、MAXIMUM_CAPACITY(最大桶容量)
3.4、MIN_TREEIFY_CAPACITY(哈希表的最小樹形化容量)
當哈希表中的容量大於這個值時,表中的桶才能進行樹形化,否則桶內元素過多時只會擴容,而不是樹形化,爲了避免由於,擴容、樹形化,選擇的衝突,這個值不能小於 4 * TREEIFY_THRESHOLD(32)
(當HashMap還很小的時候,這時候table佔用的空間也不多,擴容的方式比樹化更加節約時間,當容量大於64的時候,擴容的成本會變的越來越高,這個時候樹化比擴容更加合適個人推斷)
3.5、TREEIFY_THRESHOLD(一個桶的樹化閾值)
遵從泊松分佈裏面的結果,當正常hash分佈(滿足泊松分佈),一個桶裏面有8個節點的概率很低0.00000006,所以達到這個值進行樹化,避免高概率多個table進行樹化導致的大開銷 (在後面的泊松分佈會詳細說明)
3.6、UNTREEIFY_THRESHOLD(一個樹的鏈表還原閾值)
3.7、常量總結
常量名稱 |
常量值 |
描述 |
DEFAULT_INITIAL_CAPACITY |
1<<4 (16) |
默認初始容量-必須是2的冪,如果在構造的時候沒有傳入,默認就是16。後續會通過hash桶的負載情況進行擴容 |
DEFAULT_LOAD_FACTOR |
0.75f(負載因子值的範圍 0.25f ~ 4.0f)
|
默認負載因子,這個數值是通過線下大量數據計算得來的,可以說和泊松分佈有關,也可以說無關,後續的自動擴容和這個有關 |
MAXIMUM_CAPACITY |
1 << 30 (1073741824) |
Hash的最大值,這個數值基本上是java裏面 int的最大值,當大於這個值的時候,HashMap的桶不再擴容,新進來的數據將會發生hash碰撞 |
MIN_TREEIFY_CAPACITY |
64 (4*16) |
可以將箱子樹狀化的最小表容量(否則,如果bin中的節點太多,就會調整表的大小。)應至少爲4 * TREEIFY_THRESHOLD以避免衝突調整大小和樹化閾值之間(因爲默認初始值大小是16)。 |
TREEIFY_THRESHOLD |
8 |
每個桶中,節點轉換位樹節點的最小值(鏈表轉化爲樹節點的閾值) |
UNTREEIFY_THRESHOLD |
6 |
由樹轉換位鏈表的閾值resize()中觸發 |
4、內部類,構造方法
4.1、Node
Node的基本結構
Node本身是HashMap中bucket(桶的基本結構),本質是一個單向鏈表,但是HashMap的桶不本身是以數組的形式存在的
1、hash(final修飾) 通過HashMap hash 方法計算出來的結果
2、key (final修飾) 鍵
3、val 值(volatile修飾)
4、next 下一個節點引用(volatile修飾)
4.2、TreeNode
HashMap在JDK7的時候是單向鏈表,在JDK8的時候由於引入了TreeNode變相的使一部分的節點變成了雙向鏈表(樹節點)
1、繼承於LinkedHashMap的Entry(內有before,after節點)
2、繼承於Node(所以樹節點本身也算是Node)
3、parent 父親節點
4、left 左節點
5、right 右節點
6、prev Node節點前一個節點(雙向鏈表)
7、red 是否爲紅色
4.3、部分成員屬性
成員名稱 | 屬性描述 |
loadFactor(final修飾) | 當前HashMap對象的負載因子,HashMap一旦創建,負載因子就不能再變了 |
modCount(transient 修飾) | 當前HashMap的修改次數,官方叫做快速失敗機制(我習慣稱爲樂觀鎖) |
size(transient 修飾) | 當前HashMap的容量 |
table(transient 修飾) | 當前HashMap的table數組 |
threshold | 當前HashMap自動擴容的閾值 |
4.4、構造方法
HashMap()
// 給默認值負載因子是0.75f
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
HashMap(int initialCapacity)
//指定容量大小,並使用默認的負載因子0.75f
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
HashMap(int initialCapacity, float loadFactor)
//指定HashMap的容量和負載因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//保證table容量不會超過int最大正整數
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);
}
HashMap(Map<? extends K, ? extends V> m)
//將另一個Map放入當前map,使用默認負載因子
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
//這個方法一開始看也覺得奇怪,但是後來發現HashMap是可以被繼承,並且修改的,也就想明白了
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // 如果當前table沒有初始化,通過目標HashMap/0.75f 計算默認table大小
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//爲了保證table數量是2的冪
if (t > threshold)
threshold = tableSizeFor(t);
}
//當原HashMap不爲空的時候採取自動擴容的方式
else if (s > threshold)
resize();
//通過遍歷將目標Map的數據轉入當前Map
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);
}
}
}
5、內部實現優選
5.1、hash() 和【(n-1)& hash】 - hash相關
右移16位,讓高位和低位進行異或運算
因爲,table的長度都是2的冪,因此index僅與hash值的低n位有關,hash值的高位都被與操作置爲0了。
//hash 和 table 定位的計算方式
n = table.length;
index = (n-1) & hash;
文章參考:https://www.cnblogs.com/liujinhong/p/6576543.html(圖片來源)
5.2、tableSizeFor() 計算最小2的冪
/**
* Returns a power of two size for the given target capacity.
* 返回給定目標容量的兩個大小的冪。
* 這個方法巧妙的使用了位運算,返回當前 int的大於本身的最小2的冪
*/
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;
}
如下假入輸入的是7,7減1後是6
右移一位做或運算
右移兩位做或運算
...............
右移16位做或運算結果
結果做 + 1 操作
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
得值是:8 (大於7最小的2的冪)
爲什麼右移16位後就結束呢?按照java中對int 的定義是32位bit,1+2+4+6+8+16 = 31 ,int 31位剛好是2147483647 剛好是java裏面 int正數的最大值,從上面這個操作也看出來,HashMap table最大的大小也就是 int正整數的最大值了
那麼爲什麼一開始要減一,最後在加一呢?
int n = cap - 1;
.........
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
這裏是一個技巧,從位運算可以看出來,如果不減一,那麼上面運算的結果就是16(顯然不是結果),這麼做的目的就是保證結果是大於 cap的最小2次冪
5.3、treeifyBin()-HashMap樹化
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)
resize();
//獲取當前hash 所在的table的index
else if ((e = tab[index = (n - 1) & hash]) != null) {
// hd 頭節點 ,tl temp 遍歷使用
TreeNode<K,V> hd = null, tl = null;
do {
//將table節點替換位treenode
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);
}
}
對table樹化
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
//遍歷將這個節點下的Node全部轉換爲紅黑樹節點
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
//紅黑樹的方式插入
root = balanceInsertion(root, x);
break;
}
}
}
}
//返回到根節點
moveRootToFront(tab, root);
}
紅黑樹方式插入
//這裏建議可以直接去參照紅黑樹的實現,基本上是一致的
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
//先設置節點爲紅色
x.red = true;
//xp (父親) xpp(爺爺) xppl(爺爺左) xppr(爺爺右)
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
//如果是父親節點,爲黑色
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
//爺爺節點操作
else if (!xp.red || (xpp = xp.parent) == null)
return root;
if (xp == (xppl = xpp.left)) {
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.right) {
//左旋
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
//右旋
root = rotateRight(root, xpp);
}
}
}
}
else {
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.left) {
//右旋
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
//左旋
root = rotateLeft(root, xpp);
}
}
}
}
}
}
5.4、萬惡之源resize()-自動擴容
開始之前我們先來了解一下resize()方法的執行方式:
這裏說明一點,在JDK7的時候,執行resize()會對每個Node進行rehash操作,但是到JDK8使用異或和紅黑樹後,resize()是不需要重新計算hash值,除此之外,JDK7會在構造的時候初始化table數組,但是在JDK8中,是通過放入數據的時候,調用resize()根據當前真實的數據量進行table初始化的
final Node<K,V>[] resize() {
//獲得當前HashMap的table
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//獲取自動擴容閾值
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//判斷舊的HashMap的table容量是否達到最大值(如果是下一次擴容只能拓展爲int的最大值)
if (oldCap >= MAXIMUM_CAPACITY) {
//設置當前threshold臨界值爲 int最大值
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果沒有達到最大值,默認擴大2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//如果老的數組容量爲0,而且老的閾值大於0,則新的容量=老的閾值
else if (oldThr > 0) // initial capacity was placed in threshold
//舊的閾值就是新的table的大小(例如從 16 擴容後到 32 ,32就是16那時候的閾值)
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//計算閾值,使用默認負載因子0.75f
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果新的閾值爲0,爲新的閾值賦值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//初始化新table
@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) {
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;
//設置當前table
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
//設置當前table
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
從這裏可以看出resize()是一個開銷很大的方法,所以開發的時候要進可能避免觸發自動擴容,阿里巴巴開發手冊裏面也有對此說明:
調用HashMap#resize()並且每次遍歷的數據量(無論是內存空間,還是性能上都是一種消耗)
第一次 16
第二次 32
第三次 64
第四次 128
第五次 256
第六次 512
第七次 1024
第八次 2048
第九次 4096
5.5、putVal()-存放
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)
//如果table爲空,初始化table
n = (tab = resize()).length;
//計算下表,找到相應的table位置
if ((p = tab[i = (n - 1) & hash]) == null)
//如果找到的table爲空,對那個table初始化
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//table 是樹節點,樹化
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//計算當前table的list大小,如果超過8就進行樹化(裏面有判斷,如果當前HashMap小於64,只是擴容)
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;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//LinkedHashMap用來控制順序的
afterNodeAccess(e);
return oldValue;
}
}
//增加修改次數
++modCount;
if (++size > threshold)
//觸發自動擴容機制
resize();
afterNodeInsertion(evict);
return null;
}
5.6、getNode()獲取
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 &&
//定位節點所在的table位置
(first = tab[(n - 1) & hash]) != null) {
//快速獲取(如果當前table 就是目標,直接返回)
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)
//如果當前table是樹(通過樹的方式查找)
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;
}
5.7、modCount 和 ConcurrentModificationException
瞭解Doug Lea 的人都知道他喜歡做一個快速失敗機制的東西,比如AbstractList裏面就有modCount的東西,當然HashMap也使用上了。modCount意思就是,當前對象被修改的次數
當集合或者容器被迭代器遍歷的時候Iterator,如果發現集合本身被修改了,那麼就會快速終止遍歷,從而保證數據一致性,比如HashIterator中,從創建Iterator 的時候就會獲取一次modCount
在後面方法的時候每次操作都會去判斷這個值,比如Iterator中的remove()方法,和nextNode()方法
6、迭代器
6.1、HashIterator(內部迭代器)
HashIterator是HashMap的內部迭代器,HashMap中的各個迭代器都是繼承於它內部的結構是一個單向鏈表
1、next 下一個節點
2、current當前節點
3、expectedModCount HashMap修改次數(別人都叫快速失敗機制,我更喜歡叫樂觀鎖,文章後面會說明)
4、index當前位置
HashIterator構造
HashIterator() {
//記錄當前HashMap的修改次數
expectedModCount = modCount;
//獲取當前HashMap的所有桶
Node<K,V>[] t = table;
current = next = null;
index = 0;
//遍歷把table 放入鏈表(注意這裏是table不是Node)
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);
}
}
從這句可以看出來
expectedModCount = modCount;
在生成迭代器的時候,modCount 是賦值給了expectedModCount ,在迭代器中的操作是對expectedModCount 修改的,所以想必後續如果在HashMap的Iterator中對元素進行操作,很有可能會導致modCount和expectedModCount不一致的情況(ArrayList也有這種情況)
nextNode()方法
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
//樂觀鎖,傳說中的快速失敗機制
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
//如果當前桶沒有遍歷結束,會使用next繼續返回下一個Nodex
if ((next = (current = e).next) == null && (t = table) != null) {
//上一個table遍歷結束,換下一個table(桶)
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
6.2、HashMapSpliterator(內部類分割迭代器 )
6.2.1、EntrySpliterator
6.2.2、KeySpliterator
6.2.3、ValueSpliterator
7、樹和泊松分佈
很多時候在閱讀HashMap源碼的時候,大家都忽略了這個點,認爲每個table就應該是8的時候樹化,負載因子就應該是0.75f,但是有沒有想過爲什麼是這些值呢?下面就和大家聊聊HashMap裏面和泊松分佈有關的內容(切記這些常量是測量出來的值,但並不是和泊松分佈有關)
這句話的意思是:
由於樹節點的空間大小是普通節點的兩倍,當一個桶中的元素個數滿足閾值的時候纔會使用樹節點(TREEIFY_THRESHOLD)。當桶中的元素變化的時候(比如元素刪除,或者大小調整),會從樹變成普通模式,在使用分佈良好的hash碼的時候,樹節點將會很少被使用。
7.1、引入紅黑樹的傳說
在設計HashMap的時候,要儘可能的避免使用TreeNode 也就是除非萬不得已的時候,不會使用紅黑樹,爲什麼呢?這裏有一個傳說。相傳低版本的HashMap某次被黑客DDOS攻擊導致服務器癱瘓就是由於HashMap分佈不均導致的
從圖中可以看出,被攻擊的HashMap中的某個鏈表是可以無限長的,如果這個時候有代碼訪問使用這個HashMap,會使DDOS攻擊的效果倍增,瞬間讓整個服務器進入癱瘓狀態,所以傳說後來在JDK8中引入了紅黑樹
7.2、破壞HashMap的分佈
秉承着,不怕事情搞大的原則,爲了保證更好的使用HashMap,我們必然會去想什麼情況下或者怎麼樣才能破壞HashMap的分佈結構呢?首先我們回過頭看看Hash的在計算數組下標時候使用的方式:
n = (tab = resize()).length
........
tab[i = (n - 1) & hash])
從中可以很明細看出,是使用Hash和table大小做並(&)運算,假設我們都知道並運算的特點,那麼只要我們能保證構造的一批Object的Hash值通過&都能分配到同一個index的table裏面就行了,然後我們在看看當初hash()的計算方式
static final int hash(Object key) {
int h;
//高位和低位 16位進行異或運算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
我們知道當計算下標的時候是使用低位進行並運算(& )
所以只要保證低位相同,高位不同(相同的hash會被認爲是同一個對象),就能構造出HashMap碰撞100%的程序了,最終結果:
//隨便給一個默認值
private static int defInt = 124253;
public int hashCode() {
//將當前對象的hash合併(使用異或更容易打亂)
int hash = this.b.hashCode() ^ this.a.hashCode() ^ this.c.hashCode();
//左移8位,這裏的位數可以自己決定
hash = hash << 16;
//和默認值做或運算(讓低位運算)
return defInt | hash;
}
例如hash值:
固定的低位值
這樣就能保證每次Hash都是同一個index了
7.3、樹化的優劣
雖然這樣做很好的避免的惡意破壞HashMap分佈的情況,但是樹化就真的一定很好嗎?
從圖中可以看出來,樹節點是很沉重的(樹節點本身就需要滿足Node節點的基本屬性,並繼承於它),其次分佈良好的HashMap是可以通過位運算,通過index的方式快速定位,這個效率是遠遠勝過於紅黑樹的遍歷 ,在新value進入HashMap的時候,走hash 的 index 數度也算遠遠快於紅黑樹(紅黑樹爲了保證平衡,會進行左旋和右旋,而且紅黑樹本身是鏈表的形式存在,性能自然不足使用數組的Node table)
那麼就得到一個原則:儘量不要樹化(如果期望樹化了也就沒必要使用HashMap了可以直接使用TreeMap)
那麼HashMap就要讓正常分佈的數據儘可能少使用TreeNode,而異常的數據使用TreeNode,那麼8我們能理解了,基本上按照泊松分佈HashMap不會達到8這個量級的極限上認爲滿足泊松分佈中8個Node就等同於不存在的(0.00000006 )
7.4、HashMap中的泊松分佈
首先一上來我們又貼出了公式:
泊松分佈公式的推導:https://blog.csdn.net/ccnt_2012/article/details/81114920
首先說結論,這個公式是泊松分佈的概率密度函數,那麼HashMap用這個概率密度算了什麼內容呢?
通過泊松分佈的公式,我們計算出每個table中出現Node原始的概率。其中有這麼一句話
可以看出來了,圖中這個公式就是泊松分佈的公式,其中 = 0.5 , n=k(table list 的大小) 。從中可以看出,完美的Hash分佈的table裏面的元素是遵循泊松分佈的
7.5、負載因子
說到這裏,貌似我還是一直沒有解釋,負載因子是怎麼來的,和泊松分佈有關嗎?老實說負載因子和泊松分佈還真沒多大關係,但是說明泊松分佈之後,大家才能更加了,樹在HashMap中的低位,還有負載因子和HashMap之間的關係。傳說:這個概率是通過實驗得來的,我無意間在StackOverFlow看到一個文章
可以得出,負載因子和HashMap的性能一定程度上是負相關的
再者,我們發現在HashMap自動擴容計算閾值的時候,如果負載因子越小,那麼table空間的利於率越低,擴容的越頻繁,那麼我可以這樣認爲,負載因子越小,HashMap的性能越高,但是同時會浪費內存空間,那麼負載因子0.75f是在空間和時間上做的一個折衷,但是也有人說是根據牛頓二項定理得來的,當然到這裏的時候我就沒在往下探究了,有興趣不嫌事情搞大的盆友們可以繼續深究(也有可能這個值是 Doug Lea 一時拍腦袋給出來的)
8、HashMap 衍生Map
8.1、LinkedHashMap
8.1.1 LRU算法
8.2、ConcurrentHashMap CAS 和 分段鎖
在防止併發的情況下ConcurrentHashMap和Hashtable實現的方式不太一樣,ConcurrentHashMap使用的是分段鎖的方式,只對當前table進行上鎖,Hashtable則是大粒度鎖,根據阿姆達爾定律,ConcurrentHashMap的通過硬件加速的效果將遠遠大於Hashtable,而且在分佈良好的ConcurrentHashMap中,性能上基本上是和HashMap保持一致的
Hashtable
//在方法上面加入 synchronized
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
ConcurrentHashMap
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//尋找確定當前節點對應的table位置,請注意這個f變量
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
//當添加空節點的時候不上鎖
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//這裏是對f上鎖(很明細使用的是分段鎖)
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
CAS
在ConcurrentHashMap中自然也少不了CAS的使用,從中也能看出當對一個table操作的時候,使用了CAS的方式
CAS主要是放在putVal的時候由於不知道table index 使用不了分段鎖的場景,對已有的val操作都是使用分段鎖的方式
9、歷史HashMap的BUG
9.1、JDK7 HashMap CPU 100%(死鏈)
問題的核心點在於多線程進行擴容的時候每個線程會生成一個新的table對象,線程A生成新的table以後,線程B在線程A新生成的table上進行操作會造成死循環。
https://www.jianshu.com/p/61a829fa4e49
https://www.jianshu.com/p/ab0111c0a34b(這篇比較詳細)
9.2、JDK8 ConcurrentHashMap CPU 100%
computeIfAbsent導致CPU打滿,在new ReservationNode 的時候 hash的默認值是-3
試了一下,雖然在本地沒有100%,但是CPU確實飆高了
Map<String,String> map = new ConcurrentHashMap<>();
//第一種
//map.computeIfAbsent("AaAa", key -> map.computeIfAbsent("BBBB", key2 -> "value"));
//第二種
map.computeIfAbsent("AaAa",(String key) -> {map.put("BBBB","value"); return "value";});
官方給出的BUG描述
由於代碼的第1670行不滿足判斷,導致代碼一直死循環
這裏會初始化一個預留佔位的Node
BUG發生的位置(由於這裏的fh值等於-3導致無法退出循環)
這個預留Node的hash值默認是-3
https://bugs.openjdk.java.net/browse/JDK-8062841(openJDK bug 提交的地址)
10、JDK 7 HashMap源碼
看完了JDK8的源碼,我們這邊回顧一下JDK7HashMap的源碼(方便大家對比JDK8)
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable
{
/**
* Default number of buckets. This is the value the JDK 1.3 uses. Some
* early documentation specified this value as 101. That is incorrect.
* Package visible for use by HashSet.
*/
static final int DEFAULT_CAPACITY = 11;
/**
* The default load factor; this is explicitly specified by the spec.
* Package visible for use by HashSet.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* Compatible with JDK 1.2.
*/
private static final long serialVersionUID = 362498820763181265L;
/**
* The rounded product of the capacity and the load factor; when the number
* of elements exceeds the threshold, the HashMap calls
* <code>rehash()</code>.
* @serial the threshold for rehashing
*/
private int threshold;
/**
* Load factor of this HashMap: used in computing the threshold.
* Package visible for use by HashSet.
* @serial the load factor
*/
final float loadFactor;
/**
* Array containing the actual key-value mappings.
* Package visible for use by nested and subclasses.
*/
transient HashEntry[] buckets;
/**
* Counts the number of modifications this HashMap has undergone, used
* by Iterators to know when to throw ConcurrentModificationExceptions.
* Package visible for use by nested and subclasses.
*/
transient int modCount;
/**
* The size of this HashMap: denotes the number of key-value pairs.
* Package visible for use by nested and subclasses.
*/
transient int size;
/**
* The cache for {@link #entrySet()}.
*/
private transient Set entries;
/**
* Class to represent an entry in the hash table. Holds a single key-value
* pair. Package visible for use by subclass.
*
* @author Eric Blake <[email protected]>
*/
static class HashEntry extends AbstractMap.BasicMapEntry
{
/**
* The next entry in the linked list. Package visible for use by subclass.
*/
HashEntry next;
/**
* Simple constructor.
* @param key the key
* @param value the value
*/
HashEntry(Object key, Object value)
{
super(key, value);
}
/**
* Called when this entry is accessed via {@link #put(Object, Object)}.
* This version does nothing, but in LinkedHashMap, it must do some
* bookkeeping for access-traversal mode.
*/
void access()
{
}
/**
* Called when this entry is removed from the map. This version simply
* returns the value, but in LinkedHashMap, it must also do bookkeeping.
*
* @return the value of this key as it is removed
*/
Object cleanup()
{
return value;
}
}
/**
* Construct a new HashMap with the default capacity (11) and the default
* load factor (0.75).
*/
public HashMap()
{
this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
}
/**
* Construct a new HashMap from the given Map, with initial capacity
* the greater of the size of <code>m</code> or the default of 11.
* <p>
*
* Every element in Map m will be put into this new HashMap.
*
* @param m a Map whose key / value pairs will be put into the new HashMap.
* <b>NOTE: key / value pairs are not cloned in this constructor.</b>
* @throws NullPointerException if m is null
*/
public HashMap(Map m)
{
this(Math.max(m.size() * 2, DEFAULT_CAPACITY), DEFAULT_LOAD_FACTOR);
putAllInternal(m);
}
/**
* Construct a new HashMap with a specific inital capacity and
* default load factor of 0.75.
*
* @param initialCapacity the initial capacity of this HashMap (>=0)
* @throws IllegalArgumentException if (initialCapacity < 0)
*/
public HashMap(int initialCapacity)
{
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* Construct a new HashMap with a specific inital capacity and load factor.
*
* @param initialCapacity the initial capacity (>=0)
* @param loadFactor the load factor (> 0, not NaN)
* @throws IllegalArgumentException if (initialCapacity < 0) ||
* ! (loadFactor > 0.0)
*/
public HashMap(int initialCapacity, float loadFactor)
{
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "
+ initialCapacity);
if (! (loadFactor > 0)) // check for NaN too
loadFactor = 0.75f;
if (initialCapacity == 0)
initialCapacity = 1;
buckets = new HashEntry[initialCapacity];
this.loadFactor = loadFactor;
threshold = (int) (initialCapacity * loadFactor);
}
/**
* Returns the number of kay-value mappings currently in this Map.
*
* @return the size
*/
public int size()
{
return size;
}
/**
* Returns true if there are no key-value mappings currently in this Map.
*
* @return <code>size() == 0</code>
*/
public boolean isEmpty()
{
return size == 0;
}
/**
* Return the value in this HashMap associated with the supplied key,
* or <code>null</code> if the key maps to nothing. NOTE: Since the value
* could also be null, you must use containsKey to see if this key
* actually maps to something.
*
* @param key the key for which to fetch an associated value
* @return what the key maps to, if present
* @see #put(Object, Object)
* @see #containsKey(Object)
*/
public Object get(Object key)
{
int idx = hash(key);
HashEntry e = buckets[idx];
while (e != null)
{
if (equals(key, e.key))
return e.value;
e = e.next;
}
return null;
}
/**
* Returns true if the supplied object <code>equals()</code> a key
* in this HashMap.
*
* @param key the key to search for in this HashMap
* @return true if the key is in the table
* @see #containsValue(Object)
*/
public boolean containsKey(Object key)
{
int idx = hash(key);
HashEntry e = buckets[idx];
while (e != null)
{
if (equals(key, e.key))
return true;
e = e.next;
}
return false;
}
/**
* Puts the supplied value into the Map, mapped by the supplied key.
* The value may be retrieved by any object which <code>equals()</code>
* this key. NOTE: Since the prior value could also be null, you must
* first use containsKey if you want to see if you are replacing the
* key's mapping.
*
* @param key the key used to locate the value
* @param value the value to be stored in the HashMap
* @return the prior mapping of the key, or null if there was none
* @see #get(Object)
* @see Object#equals(Object)
*/
public Object put(Object key, Object value)
{
int idx = hash(key);
HashEntry e = buckets[idx];
while (e != null)
{
if (equals(key, e.key))
{
e.access(); // Must call this for bookkeeping in LinkedHashMap.
Object r = e.value;
e.value = value;
return r;
}
else
e = e.next;
}
// At this point, we know we need to add a new entry.
modCount++;
if (++size > threshold)
{
rehash();
// Need a new hash value to suit the bigger table.
idx = hash(key);
}
// LinkedHashMap cannot override put(), hence this call.
addEntry(key, value, idx, true);
return null;
}
/**
* Copies all elements of the given map into this hashtable. If this table
* already has a mapping for a key, the new mapping replaces the current
* one.
*
* @param m the map to be hashed into this
*/
public void putAll(Map m)
{
Iterator itr = m.entrySet().iterator();
int msize = m.size();
while (msize-- > 0)
{
Map.Entry e = (Map.Entry) itr.next();
// Optimize in case the Entry is one of our own.
if (e instanceof AbstractMap.BasicMapEntry)
{
AbstractMap.BasicMapEntry entry = (AbstractMap.BasicMapEntry) e;
put(entry.key, entry.value);
}
else
put(e.getKey(), e.getValue());
}
}
/**
* Removes from the HashMap and returns the value which is mapped by the
* supplied key. If the key maps to nothing, then the HashMap remains
* unchanged, and <code>null</code> is returned. NOTE: Since the value
* could also be null, you must use containsKey to see if you are
* actually removing a mapping.
*
* @param key the key used to locate the value to remove
* @return whatever the key mapped to, if present
*/
public Object remove(Object key)
{
int idx = hash(key);
HashEntry e = buckets[idx];
HashEntry last = null;
while (e != null)
{
if (equals(key, e.key))
{
modCount++;
if (last == null)
buckets[idx] = e.next;
else
last.next = e.next;
size--;
// Method call necessary for LinkedHashMap to work correctly.
return e.cleanup();
}
last = e;
e = e.next;
}
return null;
}
/**
* Clears the Map so it has no keys. This is O(1).
*/
public void clear()
{
if (size != 0)
{
modCount++;
Arrays.fill(buckets, null);
size = 0;
}
}
/**
* Returns true if this HashMap contains a value <code>o</code>, such that
* <code>o.equals(value)</code>.
*
* @param value the value to search for in this HashMap
* @return true if at least one key maps to the value
* @see containsKey(Object)
*/
public boolean containsValue(Object value)
{
for (int i = buckets.length - 1; i >= 0; i--)
{
HashEntry e = buckets[i];
while (e != null)
{
if (equals(value, e.value))
return true;
e = e.next;
}
}
return false;
}
/**
* Returns a shallow clone of this HashMap. The Map itself is cloned,
* but its contents are not. This is O(n).
*
* @return the clone
*/
public Object clone()
{
HashMap copy = null;
try
{
copy = (HashMap) super.clone();
}
catch (CloneNotSupportedException x)
{
// This is impossible.
}
copy.buckets = new HashEntry[buckets.length];
copy.putAllInternal(this);
// Clear the entry cache. AbstractMap.clone() does the others.
copy.entries = null;
return copy;
}
/**
* Returns a "set view" of this HashMap's keys. The set is backed by the
* HashMap, so changes in one show up in the other. The set supports
* element removal, but not element addition.
*
* @return a set view of the keys
* @see #values()
* @see #entrySet()
*/
public Set keySet()
{
if (keys == null)
// Create an AbstractSet with custom implementations of those methods
// that can be overridden easily and efficiently.
keys = new AbstractSet()
{
public int size()
{
return size;
}
public Iterator iterator()
{
// Cannot create the iterator directly, because of LinkedHashMap.
return HashMap.this.iterator(KEYS);
}
public void clear()
{
HashMap.this.clear();
}
public boolean contains(Object o)
{
return containsKey(o);
}
public boolean remove(Object o)
{
// Test against the size of the HashMap to determine if anything
// really got removed. This is necessary because the return value
// of HashMap.remove() is ambiguous in the null case.
int oldsize = size;
HashMap.this.remove(o);
return oldsize != size;
}
};
return keys;
}
/**
* Returns a "collection view" (or "bag view") of this HashMap's values.
* The collection is backed by the HashMap, so changes in one show up
* in the other. The collection supports element removal, but not element
* addition.
*
* @return a bag view of the values
* @see #keySet()
* @see #entrySet()
*/
public Collection values()
{
if (values == null)
// We don't bother overriding many of the optional methods, as doing so
// wouldn't provide any significant performance advantage.
values = new AbstractCollection()
{
public int size()
{
return size;
}
public Iterator iterator()
{
// Cannot create the iterator directly, because of LinkedHashMap.
return HashMap.this.iterator(VALUES);
}
public void clear()
{
HashMap.this.clear();
}
};
return values;
}
/**
* Returns a "set view" of this HashMap's entries. The set is backed by
* the HashMap, so changes in one show up in the other. The set supports
* element removal, but not element addition.<p>
*
* Note that the iterators for all three views, from keySet(), entrySet(),
* and values(), traverse the HashMap in the same sequence.
*
* @return a set view of the entries
* @see #keySet()
* @see #values()
* @see Map.Entry
*/
public Set entrySet()
{
if (entries == null)
// Create an AbstractSet with custom implementations of those methods
// that can be overridden easily and efficiently.
entries = new AbstractSet()
{
public int size()
{
return size;
}
public Iterator iterator()
{
// Cannot create the iterator directly, because of LinkedHashMap.
return HashMap.this.iterator(ENTRIES);
}
public void clear()
{
HashMap.this.clear();
}
public boolean contains(Object o)
{
return getEntry(o) != null;
}
public boolean remove(Object o)
{
HashEntry e = getEntry(o);
if (e != null)
{
HashMap.this.remove(e.key);
return true;
}
return false;
}
};
return entries;
}
/**
* Helper method for put, that creates and adds a new Entry. This is
* overridden in LinkedHashMap for bookkeeping purposes.
*
* @param key the key of the new Entry
* @param value the value
* @param idx the index in buckets where the new Entry belongs
* @param callRemove whether to call the removeEldestEntry method
* @see #put(Object, Object)
*/
void addEntry(Object key, Object value, int idx, boolean callRemove)
{
HashEntry e = new HashEntry(key, value);
e.next = buckets[idx];
buckets[idx] = e;
}
/**
* Helper method for entrySet(), which matches both key and value
* simultaneously.
*
* @param o the entry to match
* @return the matching entry, if found, or null
* @see #entrySet()
*/
final HashEntry getEntry(Object o)
{
if (! (o instanceof Map.Entry))
return null;
Map.Entry me = (Map.Entry) o;
Object key = me.getKey();
int idx = hash(key);
HashEntry e = buckets[idx];
while (e != null)
{
if (equals(e.key, key))
return equals(e.value, me.getValue()) ? e : null;
e = e.next;
}
return null;
}
/**
* Helper method that returns an index in the buckets array for `key'
* based on its hashCode(). Package visible for use by subclasses.
*
* @param key the key
* @return the bucket number
*/
final int hash(Object key)
{
return key == null ? 0 : Math.abs(key.hashCode() % buckets.length);
}
/**
* Generates a parameterized iterator. Must be overrideable, since
* LinkedHashMap iterates in a different order.
*
* @param type {@link #KEYS}, {@link #VALUES}, or {@link #ENTRIES}
* @return the appropriate iterator
*/
Iterator iterator(int type)
{
return new HashIterator(type);
}
/**
* A simplified, more efficient internal implementation of putAll(). The
* Map constructor and clone() should not call putAll or put, in order to
* be compatible with the JDK implementation with respect to subclasses.
*
* @param m the map to initialize this from
*/
void putAllInternal(Map m)
{
Iterator itr = m.entrySet().iterator();
int msize = m.size();
size = msize;
while (msize-- > 0)
{
Map.Entry e = (Map.Entry) itr.next();
Object key = e.getKey();
int idx = hash(key);
addEntry(key, e.getValue(), idx, false);
}
}
/**
* Increases the size of the HashMap and rehashes all keys to new
* array indices; this is called when the addition of a new value
* would cause size() > threshold. Note that the existing Entry
* objects are reused in the new hash table.
*
* <p>This is not specified, but the new size is twice the current size
* plus one; this number is not always prime, unfortunately.
*/
private void rehash()
{
HashEntry[] oldBuckets = buckets;
int newcapacity = (buckets.length * 2) + 1;
threshold = (int) (newcapacity * loadFactor);
buckets = new HashEntry[newcapacity];
for (int i = oldBuckets.length - 1; i >= 0; i--)
{
HashEntry e = oldBuckets[i];
while (e != null)
{
int idx = hash(e.key);
HashEntry dest = buckets[idx];
if (dest != null)
{
while (dest.next != null)
dest = dest.next;
dest.next = e;
}
else
buckets[idx] = e;
HashEntry next = e.next;
e.next = null;
e = next;
}
}
}
/**
* Serializes this object to the given stream.
*
* @param s the stream to write to
* @throws IOException if the underlying stream fails
* @serialData the <i>capacity</i>(int) that is the length of the
* bucket array, the <i>size</i>(int) of the hash map
* are emitted first. They are followed by size entries,
* each consisting of a key (Object) and a value (Object).
*/
private void writeObject(ObjectOutputStream s) throws IOException
{
// Write the threshold and loadFactor fields.
s.defaultWriteObject();
s.writeInt(buckets.length);
s.writeInt(size);
// Avoid creating a wasted Set by creating the iterator directly.
Iterator it = iterator(ENTRIES);
while (it.hasNext())
{
HashEntry entry = (HashEntry) it.next();
s.writeObject(entry.key);
s.writeObject(entry.value);
}
}
/**
* Deserializes this object from the given stream.
*
* @param s the stream to read from
* @throws ClassNotFoundException if the underlying stream fails
* @throws IOException if the underlying stream fails
* @serialData the <i>capacity</i>(int) that is the length of the
* bucket array, the <i>size</i>(int) of the hash map
* are emitted first. They are followed by size entries,
* each consisting of a key (Object) and a value (Object).
*/
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read the threshold and loadFactor fields.
s.defaultReadObject();
// Read and use capacity, followed by key/value pairs.
buckets = new HashEntry[s.readInt()];
int len = s.readInt();
while (len-- > 0)
{
Object key = s.readObject();
addEntry(key, s.readObject(), hash(key), false);
}
}
/**
* Iterate over HashMap's entries.
* This implementation is parameterized to give a sequential view of
* keys, values, or entries.
*
* @author Jon Zeppieri
*/
private final class HashIterator implements Iterator
{
/**
* The type of this Iterator: {@link #KEYS}, {@link #VALUES},
* or {@link #ENTRIES}.
*/
private final int type;
/**
* The number of modifications to the backing HashMap that we know about.
*/
private int knownMod = modCount;
/** The number of elements remaining to be returned by next(). */
private int count = size;
/** Current index in the physical hash table. */
private int idx = buckets.length;
/** The last Entry returned by a next() call. */
private HashEntry last;
/**
* The next entry that should be returned by next(). It is set to something
* if we're iterating through a bucket that contains multiple linked
* entries. It is null if next() needs to find a new bucket.
*/
private HashEntry next;
/**
* Construct a new HashIterator with the supplied type.
* @param type {@link #KEYS}, {@link #VALUES}, or {@link #ENTRIES}
*/
HashIterator(int type)
{
this.type = type;
}
/**
* Returns true if the Iterator has more elements.
* @return true if there are more elements
* @throws ConcurrentModificationException if the HashMap was modified
*/
public boolean hasNext()
{
if (knownMod != modCount)
throw new ConcurrentModificationException();
return count > 0;
}
/**
* Returns the next element in the Iterator's sequential view.
* @return the next element
* @throws ConcurrentModificationException if the HashMap was modified
* @throws NoSuchElementException if there is none
*/
public Object next()
{
if (knownMod != modCount)
throw new ConcurrentModificationException();
if (count == 0)
throw new NoSuchElementException();
count--;
HashEntry e = next;
while (e == null)
e = buckets[--idx];
next = e.next;
last = e;
if (type == VALUES)
return e.value;
if (type == KEYS)
return e.key;
return e;
}
public void remove()
{
if (knownMod != modCount)
throw new ConcurrentModificationException();
if (last == null)
throw new IllegalStateException();
HashMap.this.remove(last.key);
last = null;
knownMod++;
}
}
}
11、總結