当前位置: 首页 > news >正文

集合框架2

hashmap最常问的十个问题,并给出回答
以下是HashMap最常被问到的十个核心问题及精准解答,覆盖原理、特性与关键操作:

  1. HashMap 是什么?它的核心作用是什么?

HashMap 是 Java 中基于哈希表实现的 Map 接口实现类,核心作用是存储 键值对(Key-Value),并通过哈希算法实现键到值的快速查找、插入和删除,平均时间复杂度为 O(1)。

  1. HashMap 的底层数据结构是什么?JDK 1.7 和 1.8 有什么区别?

    • JDK 1.7:底层是 数组 + 链表(数组存储哈希桶,链表解决哈希冲突)。

    • JDK 1.8:底层是 数组 + 链表 + 红黑树(当链表长度超过阈值 8,且数组容量 ≥ 64 时,链表会转为红黑树,提升查询效率;当链表长度 ≤ 6 时,红黑树会退化为链表)。

  2. HashMap 的哈希冲突是如何产生的?如何解决?

    • 产生原因:不同 Key 经过哈希函数计算后,得到的 哈希值相同,或哈希值不同但对数组长度取模后,得到的 数组索引(桶位)相同。

    • 解决方式:

    1. 链地址法(JDK 核心方案):冲突的 Key 对应的 Value 以链表/红黑树形式存储在同一桶位。

    2. 扰动函数:对 Key 的哈希值进行多次异或和移位,降低哈希冲突概率(JDK 1.8 简化为 1 次扰动,但仍有效)。

  3. HashMap 的 Key 可以为 null 吗?Value 呢?

    • Key:可以为 null,但 只能有 1 个(因为 HashMap 不允许 Key 重复,若多次 put null Key,后值会覆盖前值)。

    • Value:可以为 null,且 允许有多个(不同 Key 可对应相同的 null Value)。

  4. HashMap 为什么线程不安全?有哪些线程安全的替代类?

    • 线程不安全原因:

    ◦ JDK 1.7 中,多线程扩容时可能导致 链表死循环(头插法导致链表反转)。

    ◦ 多线程并发 put 时,可能出现 数据覆盖(如两个线程同时判断同一桶位为空,均执行插入)。

    • 线程安全替代类:

    ◦ ConcurrentHashMap:JDK 1.8 基于 CAS + synchronized 实现,性能优于 Hashtable。

    ◦ Hashtable:通过 全局 synchronized 锁 保证安全,性能较差,已较少使用。

    ◦ Collections.synchronizedMap(new HashMap<>()):对 HashMap 加锁包装,本质是全局锁,性能一般。

  5. HashMap 的初始容量、负载因子、扩容机制是什么?

    • 初始容量:默认 16(必须是 2 的幂,便于通过“哈希值 & 数组长度-1”快速计算桶位,等价于取模且效率更高)。

    • 负载因子:默认 0.75(衡量数组“满”的程度,0.75 是时间效率与空间效率的平衡值)。

    • 扩容机制:当 HashMap 中元素个数(size)≥ 容量 × 负载因子时,触发扩容,新容量为原容量的 2 倍,并重新计算所有元素的桶位(rehash)后迁移。

  6. HashMap 的 Key 为什么推荐用不可变类(如 String、Integer)?

因为 Key 的 哈希值会影响存储位置,若 Key 是可变类(如自定义类未重写 hashCode/equals):

•	当 Key 对象被修改后,其哈希值可能变化,导致后续无法通过原 Key 找到对应的 Value(原桶位与新哈希值计算的桶位不一致)。•	不可变类(如 String)的哈希值在创建后固定,能保证 Key 对应的存储位置稳定,避免查找失效。
  1. HashMap 中 hashCode() 和 equals() 方法的作用是什么?必须同时重写吗?

    • hashCode():计算 Key 的哈希值,用于确定元素在数组中的 桶位索引。

    • equals():当多个 Key 哈希冲突(桶位相同)时,用于判断 Key 是否“真正相等”,以确定是否覆盖 Value 或找到目标 Value。

    • 必须同时重写:若只重写 equals() 而不重写 hashCode(),可能导致“逻辑相等的 Key 哈希值不同”,从而被存入 HashMap 的不同桶位,违背 Map“Key 唯一”的规则;若只重写 hashCode() 而不重写 equals(),则可能导致“哈希值相同但逻辑不相等的 Key 被判定为相等”,出现错误覆盖。

  2. HashMap 和 Hashtable 的核心区别是什么?
    对比维度 HashMap Hashtable
    线程安全 不安全 安全(全局 synchronized)
    Key/Value 为 null Key 可 null(1 个),Value 可 null(多个) Key 和 Value 均不可为 null
    初始容量/扩容 初始 16,扩容为 2 倍 初始 11,扩容为 2 倍+1
    哈希计算 哈希值 & (容量-1)(需容量为 2 的幂) 哈希值 % 容量(容量可为非 2 的幂)

  3. JDK 1.8 中 HashMap 做了哪些优化?

    1. 数据结构优化:引入红黑树,解决链表查询效率低的问题(链表查询 O(n),红黑树 O(logn))。

    2. 扩容机制优化:移除 JDK 1.7 的“头插法”,改用“尾插法”,避免多线程扩容导致的链表死循环。

    3. 哈希计算优化:简化扰动函数(1 次异或 + 移位),在保证哈希分散性的同时提升计算效率。

    4. 查询效率优化:红黑树的引入,使大量哈希冲突场景下的查询、删除效率大幅提升。

再来十个hashmap源码层面最常问的问题,并给出回答
以下是HashMap源码层面最常问的十个核心问题及精准解答,聚焦底层实现细节与关键逻辑:

  1. HashMap 中 table 数组的初始化时机是什么?

table 数组(存储哈希桶的数组)并非在构造方法中初始化,而是在 第一次调用 put() 方法时,通过 resize() 方法完成初始化(懒加载机制,避免创建空HashMap时浪费内存)。

  1. 源码中如何计算 Key 的桶位索引?核心公式是什么?

核心是通过“哈希值扰动 + 按位与”计算,确保索引在 table 数组范围内,公式分两步:

1.	计算扰动后哈希值:JDK 1.8 中为 hash = key == null ? 0 : (h = key.hashCode()) ^ (h >>> 16)(将 hashCode 高16位与低16位异或,减少哈希冲突)。2.	计算桶位索引:index = (table.length - 1) & hash(利用数组长度为2的幂的特性,等价于 hash % table.length,但按位与效率更高)。
  1. put() 方法的核心源码逻辑是什么?

    1. 若 table 未初始化,调用 resize() 初始化。

    2. 计算 Key 的桶位索引,若该桶位为空,直接创建 Node 存入(新节点)。

    3. 若桶位非空:

    ◦ 若桶位首个节点的 Key 与当前 Key 相等(hash 相同且 equals 为 true),标记为“待覆盖节点”。

    ◦ 若桶位是红黑树(TreeNode),调用 putTreeVal() 插入或覆盖。

    ◦ 若桶位是链表,遍历链表:

    ◦ 找到 Key 相等的节点,标记为“待覆盖节点”并跳出。

    ◦ 遍历到链表尾部,创建新 Node 插入尾部(JDK 1.8 尾插法),若链表长度 ≥ 8,调用 treeifyBin() 尝试转为红黑树。

    1. 若存在“待覆盖节点”,用新 Value 覆盖旧 Value,并返回旧 Value。

    2. 若插入新节点,size 加1,若 size ≥ 阈值,调用 resize() 扩容。

  2. resize() 方法的核心作用与源码逻辑是什么?

核心作用:初始化 table 数组,或在元素数量超阈值时扩容(新容量为原容量2倍),并迁移所有节点到新桶位。
源码逻辑:

1.	计算新容量(原容量为0时,新容量=初始容量16;否则新容量=原容量×2)和新阈值(新容量×负载因子)。2.	创建新 table 数组(容量为新容量)。3.	遍历旧 table 数组,迁移每个桶位的节点:◦	若桶位是单个节点,直接计算新桶位并放入新 table。◦	若桶位是链表,拆分链表为“原索引链”和“原索引+旧容量链”(利用 hash & 旧容量 判断,避免重新计算哈希),分别放入新 table 对应桶位。◦	若桶位是红黑树,按红黑树规则拆分并迁移,必要时退化为链表。4.	新 table 替换旧 table,更新阈值。
  1. JDK 1.8 为什么将链表转为红黑树的阈值设为 8?又为什么退化为链表的阈值设为 6?

    • 转红黑树阈值 8:基于“泊松分布”统计,HashMap 中链表长度达到 8 的概率极低(约 0.00000006),此时链表查询效率已较差(O(n)),转为红黑树(O(logn))能显著提升性能,同时避免过早转树浪费内存。

    • 退化为链表阈值 6:设置“间隔 2”的阈值(8→6),是为了避免链表与红黑树频繁转换(如长度在 7 和 8 之间波动时,反复转树/转链表),减少性能损耗。

  2. treeifyBin() 方法的作用是什么?它一定会将链表转为红黑树吗?

    • 核心作用:尝试将链表结构的桶位转为红黑树结构,优化查询效率。

    • 不一定转树:源码中会先判断 table 数组容量,若 容量 < 64,会先调用 resize() 扩容(而非直接转树),因为此时扩容成本更低,且扩容后链表会被拆分,可能无需转树;只有当容量 ≥ 64 且链表长度 ≥ 8 时,才会真正转为红黑树。

  3. HashMap 中 Node 类和 TreeNode 类的关系是什么?各自的核心属性有哪些?

    • 关系:TreeNode 是 Node 的子类(static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V>,而 LinkedHashMap.Entry 又继承 HashMap.Node),用于红黑树存储。

    • 核心属性:

    ◦ Node:hash(Key的哈希值)、key、value、next(链表下一个节点的引用)。

    ◦ TreeNode:除继承 Node 的属性外,新增红黑树相关属性:parent(父节点)、left(左子节点)、right(右子节点)、prev(链表前驱节点,用于红黑树转链表)、red(节点颜色,true为红,false为黑)。

  4. 源码中如何处理 Key 为 null 的情况?

在 put() 和 get() 方法中均有单独处理:

•	put(null, value):计算哈希值时直接赋值 hash = 0,桶位索引为 0 & (table.length - 1) = 0,即固定存入 table[0] 桶位,后续逻辑与非null Key一致(判断冲突、覆盖或插入)。•	get(null):直接定位到 table[0] 桶位,遍历该桶位的链表/红黑树,找到 Key 为 null 的节点并返回 Value(若不存在则返回 null)。
  1. remove() 方法的源码逻辑是什么?如何处理红黑树节点的删除?

    1. 计算 Key 的哈希值和桶位索引,若 table 未初始化或桶位为空,直接返回 null。

    2. 遍历桶位节点(链表/红黑树),找到 Key 相等的目标节点:

    ◦ 若桶位是链表,找到目标节点后,通过修改前驱节点的 next 引用删除(跳过目标节点)。

    ◦ 若桶位是红黑树,调用 removeTreeNode() 方法删除目标节点,删除后会检查红黑树平衡,若节点数 ≤ 6,会将红黑树退化为链表。

    1. 若找到目标节点,记录其 Value,size 减1,返回旧 Value;未找到则返回 null。
  2. JDK 1.8 中 hashCode() 相同但 equals() 不同的 Key,会如何存储?

会以“哈希冲突”的形式存储在同一桶位:

1.	计算哈希值时,两者 hashCode() 相同,经扰动后 hash 仍相同,桶位索引一致。2.	存入时,因 equals() 不同,不会覆盖已有节点,而是以链表(或红黑树)的形式追加到该桶位中(链表尾部或红黑树的对应位置)。3.	查询时,会先定位到该桶位,再通过 equals() 遍历比较,找到真正匹配的 Key 对应的 Value。
http://www.zskr.cn/news/528.html

相关文章:

  • [机器人] 产业研究之【人形机器人】
  • 因果图灵测试(Causal Turing Test, CTT),为判断AGI是否真正实现的唯一终极标准。
  • 1111
  • Codeforces Round 1048 (Div. 2)
  • 世界最顶级的游戏网络联机框架——NetCode for Entity
  • 理解Redis线程模型
  • Prometheus监控harbor仓库
  • kubernetes集群重置部署(四)
  • 第一次作业
  • windows将服务器文件夹映射到windows本地
  • [huggingface] huggingface 有和 `git clone` 一样方便的命令
  • 计数杂题选刷 Part II
  • Rust异步运行时最小实现 - extreme 分享
  • MIDI简谱编辑器1.1程序代码QZQ-2025-8-20
  • p型编码
  • OTA 升级问题的分析
  • P3195 [HNOI2008] 玩具装箱
  • 模拟题
  • 自我介绍与软工五问
  • DAY2
  • Discipline
  • 建立本地仓库
  • 长乐一中 CSP-S 2025 提高级模拟赛 Day1
  • 202310_FSCTF_DoYouKnowGCD?
  • 你的中间件一团糟-是时候修复它了-️
  • 告别框架臃肿-我如何在不牺牲性能的情况下重新发现简单之美
  • Typora
  • ARC205_B Triangle Toggle题解
  • Anthropic 封禁中国资本背景企业使用Claude!国内AI编程选择将何去何从?
  • ARC137E