HashMap JDK8 优化
面试提问
"JDK8 对 HashMap 做了哪些优化?红黑树阈值是多少?"
JDK7 vs JDK8 对比
| 对比项 | JDK7 | JDK8 |
|---|---|---|
| 底层结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
| 链表插入 | 头插法 | 尾插法 |
| 扩容优化 | 重新计算所有位置 | 原位置或原位置+旧容量 |
| 哈希计算 | 9次扰动 | 1次扰动(高16位异或) |
红黑树优化
转换阈值
// 链表转红黑树
static final int TREEIFY_THRESHOLD = 8;
// 红黑树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 转红黑树的最小数组容量
static final int MIN_TREEIFY_CAPACITY = 64;为什么是 8?
链表长度服从泊松分布,长度为 8 的概率约为 0.00000006,几乎不可能达到。设置 8 是一个平衡选择:
- 太小:频繁转换,性能下降
- 太大:链表过长,查询效率低
转换条件
// 同时满足两个条件才转红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) { // 链表长度 >= 8
if (tab.length >= MIN_TREEIFY_CAPACITY) // 数组容量 >= 64
treeifyBin(tab, hash);
else
resize(); // 容量不够,先扩容
}查询效率对比
| 结构 | 时间复杂度 |
|---|---|
| 链表 | O(n) |
| 红黑树 | O(log n) |
当链表长度为 8 时:
- 链表:最多比较 8 次
- 红黑树:最多比较 4 次(log₂8 = 3,实际略多)
头插法 vs 尾插法
JDK7 头插法问题
// JDK7 头插法
void transfer(Entry[] newTable) {
for (Entry<K,V> e : table) {
while (null != e) {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i]; // 头插
newTable[i] = e;
e = next;
}
}
}问题:多线程扩容时可能导致链表成环,get() 时死循环。
JDK8 尾插法
// JDK8 尾插法
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}优点:保持链表顺序,避免成环问题。
扩容优化
JDK7 扩容
// 每个元素重新计算位置
int index = hash(key) % newCapacity;JDK8 扩容优化
// 扩容后元素位置:原位置 或 原位置 + 旧容量
if ((e.hash & oldCap) == 0) {
// 位置不变
} else {
// 位置 = 原位置 + 旧容量
}原理
假设旧容量为 16(10000),扩容后为 32(100000)。
扩容后计算位置时,只看新增的高位:
- 如果 hash 高位为 0:位置不变
- 如果 hash 高位为 1:位置 = 原位置 + 16
// 示例:hash = 18 (10010)
// 旧数组:18 & 15 = 2 (位置 2)
// 新数组:18 & 31 = 18 (位置 18)
// 实际:2 + 16 = 18
// 示例:hash = 2 (00010)
// 旧数组:2 & 15 = 2 (位置 2)
// 新数组:2 & 31 = 2 (位置 2)
// 实际:位置不变哈希计算优化
JDK7
// 9次扰动
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}JDK8
// 1次扰动:高16位异或低16位
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}目的:让高位的特征参与运算,减少哈希冲突。
put 流程(JDK8)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1. 数组为空,初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2. 计算位置,为空则直接插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 3. key 相同,直接覆盖
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 4. 红黑树节点
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 5. 链表节点
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 链表转红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 6. 找到相同 key,覆盖 value
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
return oldValue;
}
}
++modCount;
// 7. 检查是否需要扩容
if (++size > threshold)
resize();
return null;
}面试要点总结
| 问题 | 答案要点 |
|---|---|
| JDK8 优化? | 红黑树、尾插法、扩容优化 |
| 红黑树阈值? | 链表转树 8,树转链表 6 |
| 为什么是 8? | 泊松分布,概率极低 |
| 头插法问题? | 多线程扩容链表成环 |
| 扩容优化原理? | 元素在原位置或原位置+旧容量 |
参考资料
- HashMap 源码 (opens in a new tab)
- 《Java 集合框架》