知识模块
☕ Java 知识模块
六、Java 新特性
HashMap JDK8 优化

HashMap JDK8 优化

面试提问

"JDK8 对 HashMap 做了哪些优化?红黑树阈值是多少?"


JDK7 vs JDK8 对比

对比项JDK7JDK8
底层结构数组 + 链表数组 + 链表 + 红黑树
链表插入头插法尾插法
扩容优化重新计算所有位置原位置或原位置+旧容量
哈希计算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?泊松分布,概率极低
头插法问题?多线程扩容链表成环
扩容优化原理?元素在原位置或原位置+旧容量

参考资料