面试官问我:HashMap的扩容机制,我从jdk1.7和1.8两个版本来介绍

面试官问我:HashMap的扩容机制,你需要详细分析

在Java开发中,HashMap是一个非常常用的数据结构,而它的扩容机制是面试中经常被问到的一个知识点。今天我们就来详细分析一下HashMap的扩容机制,包括它的触发条件、实现原理以及JDK不同版本中的优化。

1. HashMap的基本结构

在深入扩容机制之前,先简单回顾一下HashMap的底层结构。HashMap基于哈希表实现,内部主要由以下几个部分组成:

  • 数组(table):存储键值对的主要结构,也叫桶(bucket)。
  • 链表:当哈希冲突发生时,同一个桶中的元素会以链表形式存储。
  • 红黑树(JDK 1.8+):当链表长度超过一定阈值(默认8)时,链表会转为红黑树以提升查询效率。

HashMap的核心属性包括:

  • capacity:数组的容量,默认初始值为16。
  • loadFactor:负载因子,默认值为0.75。
  • threshold:扩容阈值,计算公式为 capacity * loadFactor
  • size:当前存储的键值对数量。

2. 扩容的触发条件

HashMap的扩容并不是在任意时刻触发的,它有明确的触发条件。当满足以下条件时,HashMap会进行扩容:

  1. 当前元素数量超过阈值 :即 size >= threshold。当插入新元素后,size 超过 capacity * loadFactor,就会触发扩容。
  2. 特殊情况(JDK 1.8+) :即使 size 未达到阈值,如果某个桶中的链表长度达到8,且当前数组容量小于64,HashMap会优先进行扩容而不是立即转为红黑树。

扩容的核心目的是为了避免哈希冲突过于频繁,保证HashMap的性能。

3. 扩容的具体过程

HashMap的扩容主要涉及以下几个步骤:

3.1 容量翻倍

每次扩容时,HashMap的容量会翻倍。例如,初始容量为16,扩容后变为32,再次扩容则变为64,以此类推。容量始终是2的幂,这是为了方便通过位运算优化哈希计算。

新的容量计算公式:

ini 复制代码
newCapacity = oldCapacity << 1  // 左移一位,相当于乘以2

3.2 创建新数组

扩容时会创建一个新的数组(newTable),其大小为新计算出的容量。

3.3 重新分配元素(rehash)

旧数组中的所有元素需要重新分配到新数组中。这个过程称为"rehash"。由于容量变大了,元素的哈希位置可能会发生变化。具体步骤如下:

  1. 遍历旧数组:逐个检查每个桶中的元素。
  2. 重新计算索引 :根据新容量重新计算每个键的哈希值对应的索引。
    • 在JDK 1.7中,会完整地重新计算每个键的哈希值并取模。
    • 在JDK 1.8中,优化了这一过程,利用了容量为2的幂的特性,通过位运算判断元素在新数组中的位置(详见下文优化部分)。
  3. 迁移元素:将元素放入新数组的对应位置。如果原来是链表或红黑树结构,也会保留这种结构。

3.4 更新阈值

扩容完成后,更新 thresholdnewCapacity * loadFactor,为下一次扩容做准备。

4. JDK 1.7 vs JDK 1.8 的扩容优化

HashMap的扩容机制在JDK 1.8中进行了显著优化,主要体现在以下两点:

4.1 rehash 的高效实现

  • JDK 1.7 :每次扩容都需要对所有键重新计算哈希值并取模(hash % newCapacity),效率较低。
  • JDK 1.8 :利用容量为2的幂的特性,只需判断每个键的哈希值与旧容量的位运算结果:
    • 如果 hash & oldCapacity == 0,元素在新数组中的位置不变。
    • 如果 hash & oldCapacity != 0,元素在新数组中的位置为 原位置 + oldCapacity。 这种方法避免了完整的哈希重新计算,大幅提升了扩容效率。

4.2 多线程问题修复

  • JDK 1.7:在多线程环境下,扩容可能导致链表形成环,引发死循环。这是由于1.7中链表迁移时采用"头插法"。
  • JDK 1.8:改为"尾插法"迁移链表,同时优化了红黑树的引入,解决了这个问题。虽然HashMap本身仍非线程安全,但在扩容时的稳定性有所提高。

5. 代码层面的分析

以JDK 1.8为例,扩容的核心方法是 resize(),以下是简化的逻辑:

java 复制代码
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;

    // 计算新容量和新阈值
    if (oldCap > 0) {
        newCap = oldCap << 1; // 容量翻倍
        newThr = oldCap * loadFactor;
    } else {
        newCap = DEFAULT_INITIAL_CAPACITY; // 默认16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }

    // 创建新数组
    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;
                        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;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    threshold = newThr;
    return newTab;
}

从代码中可以看到,JDK 1.8通过位运算和链表分流(低位和高位链表)优化了扩容过程。

6. 总结

HashMap的扩容机制是其性能和效率的关键。简单来说:

  • 触发条件size 超过 threshold 或特定链表长度条件。
  • 过程:容量翻倍 → 创建新数组 → 元素重新分配 → 更新阈值。
  • 优化:JDK 1.8 通过位运算和尾插法提升了效率和稳定性。
相关推荐
追逐时光者28 分钟前
C#/.NET/.NET Core技术前沿周刊 | 第 32 期(2025年3.24-3.31)
后端·.net
uhakadotcom29 分钟前
轻松掌握XXL-JOB:分布式任务调度的利器
后端·面试·github
小杨40431 分钟前
springboot框架项目实践应用十三(springcloud alibaba整合sentinel)
spring boot·后端·spring cloud
程序员一诺1 小时前
【Python使用】嘿马python数据分析教程第1篇:Excel的使用,一. Excel的基本使用,二. 会员分析【附代码文档】
后端·python
神奇侠20241 小时前
快速入手-基于Django-rest-framework的serializers序列化器(二)
后端·python·django
Asthenia04121 小时前
基于Segment-Mybatis的:分布式系统中主键自增拦截器的逻辑分析与实现
后端
Asthenia04121 小时前
Seata:为微服务项目的XID传播设计全局的RequestInterceptor-将XID传播与具体FeignClient行为解耦
后端
无奈何杨1 小时前
Docker/Compose常用命令整理总结
后端
搬砖的阿wei1 小时前
从零开始学 Flask:构建你的第一个 Web 应用
前端·后端·python·flask
草巾冒小子2 小时前
查看pip3 是否安装了Flask
后端·python·flask