Java 中 HashMap 的扩容机制是怎样的?


Java HashMap 扩容机制详解


一、触发条件

HashMap 在以下两种情况下触发扩容:

  1. 元素数量超过阈值
    阈值(threshold)= 当前容量(capacity) × 负载因子(loadFactor,默认0.75)。
    例如:容量为16时,阈值=12,当元素数量超过12时触发扩容。

  2. 链表转红黑树时的容量不足
    当链表长度≥8且当前数组长度<64时,优先扩容而非树化。


二、扩容流程
  1. 计算新容量

    • 默认扩容为原容量的 2倍newCap = oldCap << 1)。
    • 若原容量为0(首次初始化),默认初始容量为16。
    • 若超过最大容量限制(MAXIMUM_CAPACITY = 1<<30),则不再扩容。
  2. 创建新数组

    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    
  3. 数据迁移(Rehash)
    遍历旧数组中的每个桶(bucket),重新计算元素在新数组中的位置:

    • 链表迁移:JDK8优化为高低位拆分(避免全链表遍历)。
    • 红黑树迁移:若树节点数≤6,退化为链表。
  4. 更新阈值

    threshold = (int)(newCap * loadFactor);
    

三、源码关键逻辑(JDK17)
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;

    // 1. 计算新容量和阈值
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 新容量 = 原容量 * 2
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                  oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // 阈值也翻倍
    } else if (oldThr > 0) // 初始容量由阈值决定(构造函数指定)
        newCap = oldThr;
    else {               // 默认初始化(容量16,阈值12)
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }

    // 2. 处理新阈值计算(如原数组为空且阈值未计算)
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;

    // 3. 创建新数组并迁移数据
    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 { // 链表高低位拆分优化
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 判断高位是否为0(是否需要迁移到新位置)
                        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;
                    }
                    // 高位链表迁移到新索引(原索引 + oldCap)
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

四、扩容优化(JDK8+)
  1. 高低位拆分(Bitwise Optimization)
    通过 (e.hash & oldCap) == 0 判断元素是否留在原位置或迁移到新位置(原索引 + oldCap)。

    • 原理:新容量为旧容量2倍,哈希值高位决定是否需要位移。
    • 优势:避免重新计算哈希,仅用位运算即可确定新位置,时间复杂度降为O(n)而非O(n²)。
  2. 红黑树迁移优化

    • 若树节点数≤6,退化为链表。
    • 否则拆分树结构,减少迁移后的树高度。

五、性能影响与优化建议
场景性能影响优化建议
频繁触发扩容数据复制耗时,引发GC压力初始化时指定合理容量(如new HashMap<>(1024)
超大容量初始化内存浪费(数组预分配)动态扩容或使用trimToSize()
负载因子设置过高哈希碰撞概率增加,查询效率降低根据业务调整(如0.6提高查询性能)

示例:初始化容量计算
若预计存储1000个元素,负载因子0.75:

int initialCapacity = (int) Math.ceil(1000 / 0.75); // 1334 → 2048(最近的2^n)
Map<String, Object> map = new HashMap<>(2048);

六、常见问题
  1. 为什么默认负载因子是0.75?

    • 平衡时间(查询效率)与空间(数组利用率)的折中结果。
    • 数学推导表明,在泊松分布下,0.75时哈希碰撞概率较低。
  2. 扩容是否线程安全?

    • HashMap非线程安全,并发扩容可能导致数据丢失或死循环(JDK7及之前版本)。
  3. 如何避免多次扩容?

    • 初始化时预估容量(initialCapacity = expectedSize / loadFactor + 1)。

七、对比 Hashtable 扩容
特性HashMapHashtable
扩容倍数2倍2倍+1(奇容量减少哈希碰撞)
触发条件size > thresholdsize >= threshold
线程安全非安全(需外部同步)安全(但性能差)

总结

HashMap的扩容机制通过2倍动态扩容高低位拆分优化,平衡了哈希碰撞概率与内存利用率。理解其实现细节有助于:

  1. 避免频繁扩容带来的性能损耗
  2. 合理初始化容量和负载因子
  3. 在并发场景中选择ConcurrentHashMap替代
  4. 优化哈希函数减少碰撞概率

通过源码分析和实际场景结合,能够更精准地优化HashMap的使用,提升系统性能。

### JavaHashMap扩容机制 #### 扩容触发条件 当向 `HashMap` 插入键值对时,如果当前的负载因子(load factor)乘以初始容量不足以容纳新的条目,则会触发扩容操作。默认情况下,`HashMap` 的初始容量为 16,负载因子为 0.75。因此,默认的最大大小为 \(16 \times 0.75 = 12\)。一旦桶中的节点数超过此阈值,就会执行扩容[^1]。 #### 扩容过程 扩容的核心在于创建一个新的数组并将旧数组中的数据重新映射到新数组中。以下是具体的过程: 1. **创建更大的数组**: 新数组的长度通常是原数组长度的两倍。例如,如果原始数组长度为 16,则扩容后的数组长度变为 32。 2. **重新计算索引位置**: 对于每一个原有的键值对,其在新数组中的位置由该键的哈希值决定。由于数组变大了,部分键可能会被分配到不同的槽位上。这一步骤称为“重哈希”或“再散列”。 3. **迁移数据**: 将所有的键值对从原来的数组迁移到新的数组中。对于链表形式存储的情况,在迁移过程中还可能涉及树化处理(仅适用于 JDK 8 或更高版本),即当某个桶内的链表过长时将其转换成红黑树结构以提高查询效率[^2]。 4. **更新引用关系**: 完成上述步骤后,最后一步就是让 `table` 字段指向这个新构建好的更大尺寸的数组实例。 #### 并发问题 需要注意的是,`HashMap` 在多线程环境下的行为是不可预测且危险的。这是因为多个线程可以同时尝试修改同一个 `HashMap` 实例的内容,而没有任何同步措施保护这些更改动作之间的顺序性和一致性。这种缺乏控制的状态可能导致诸如无限循环等问题的发生。为了应对这种情况,JDK 提供了一个名为 `ConcurrentHashMap` 的替代品,它利用更精细粒度上的锁定策略以及无锁算法来保障高并发场景下容器的安全访问[^3]。 ```java // 示例代码展示如何初始化并扩展一个简单的 HashMap import java.util.HashMap; public class Main { public static void main(String[] args) throws Exception { // 创建一个空的 HashMap HashMap<String, Integer> map = new HashMap<>(4); // 添加一些元素直到达到临界点从而引发自动扩容 for (int i=0;i<10;i++) { map.put("key"+i,i); } System.out.println(map.size()); } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只蜘猪

感谢!!!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值