【集合框架】之深入分析HashMap
阿新 • • 發佈:2019-02-05
提出並解決問題如下:
問題1:初始容量為什麼是16,為什麼必須是2的冪?
問題2: hash
方法為什麼是無符號右移16位?
問題3:
問題4:
問題5:
HashMap
- 非執行緒安全
- 繼承於AbstractMap
- 實現了Map、Cloneable、java.io.Serializable介面
- 基於陣列+連結串列+紅黑樹
重要物件
- DEFAULT_INITIAL_CAPACITY :預設的初始容量是16,必須是2的冪
- MAXIMUM_CAPACITY :最大容量,1<<30
- table:Entry[]陣列型別,每一個Entry本質上是一個單向連結串列
- size:HashMap的大小,鍵值對的數量
- threshold:HashMap的閾值,threshold=容量*載入因子
- DEFAULT_LOAD_FACTOR:預設載入因子0.75
- loadFactor:實際載入因子
- modCount:HashMap被改變的次數,用來實現fail-fast機制
- HashMap擴容時,將容量變為原來的2倍
- TREEIFY_THRESHOLD :樹形和列表的閾值,預設8
- UNTREEIFY_THRESHOLD :樹形轉換回鏈式的閾值,預設6
- MIN_TREEIFY_CAPACITY :雜湊表的最小樹形化容量,預設64
問題1:初始容量為什麼是16,為什麼必須是2的冪?
- 指定為16是從效能考慮。避免重複計算
- 陣列的長度總是 2 的冪,使Hash雜湊更均勻
HashMap
的hash
函式通過hash & (table.length - 1)
來得到該物件的index
.僅與hash
值的低n位有關
因為使用的掩碼是2的次冪,高於掩碼的位組成的雜湊集合總是衝突,所以把高位移到低位。
混合原始雜湊碼的高位和低位,以此來加大低位的隨機性。
構造方法
可指定初始容量和負載因子,或從其他Map初始化
核心方法
hash()
key的hash值高16位不變,低16位與高16位異或作為key的最終hash值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
問題2: hash
方法為什麼是無符號右移16位?
設計者權衡了speed, utility, and quality
在JDK1.7中,使用四次移位
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
tableSizeFor()
返回一個比給定整數大且最接近的2的冪次方整數
問題3: tableSizeFor
如何實現?
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;
}
先取n-1,五次右移後做或運算
超過最大值取最大值,否則取n-1
作為閾值threshold
- 第一次右移1位:最高位前2位置1
- 第二次右移2位:最高位前4位置1
- 第三次右移4位:最高位前8位置1
- 第四次右移8位:最高位前16位置1
- 第五次右移8位:最高位前32位置1
put
通過對key的hashCode()進行hashing,並計算下標( n-1 & hash),從而獲得buckets的位置。如果產生碰撞,則利用key.equals()方法去連結串列或樹中去查詢對應的節點.
public V put(K key, V value) {
return putVal(hash(key), key, value, 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;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
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;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//到這裡,說明陣列該位置上是一個連結串列
for (int binCount = 0; ; ++binCount) {
// 插入到連結串列的最後面(Java7 是插入到連結串列的最前面)
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果在該連結串列中找到了"相等"的 key(== 或 equals)
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//e!=null 說明存在舊值的key與要插入的key"相等"
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;
}
具體過程
如果table為空或大小為0,觸發resize
,用預設值初始化
用(n - 1) & hash
找到buckets,為空則在此插入
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
如果產生碰撞,取出該節點p
如果hash
相等且(key
==
或equal
),即就是要找的節點e
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
如果不是要找的節點,就判斷p是紅黑是還是連結串列節點,呼叫不同的插入方法
如果插入後超過8個,會觸發 treeifyBin將連結串列轉換為紅黑樹
如果在該連結串列中找到了相等的 key(== 或 equals)
如果e!=null
說明存在舊值的key與要插入的key 相等
進行 “值覆蓋”,然後返回舊值
判斷閾值,決定是否擴容
未完待續