HashMap 的 remove 方法是如何实现的?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

面试官提出这个问题,通常旨在考察以下几个层面,而不仅仅是一个简单的 API 调用:

  1. HashMap 基础操作的了解:候选人是否清楚 remove 方法的签名、返回值含义。
  2. HashMap 核心数据结构的理解:是否理解其底层 数组 + 链表/红黑树 的结构,以及 remove 操作如何在这个结构上定位元素。
  3. 对哈希冲突解决机制的掌握:当发生哈希冲突形成链表或树时,remove 如何正确地从链表或红黑树中删除节点,并维护数据结构的完整性。
  4. HashMap 动态特性与相关机制的认识:是否了解删除操作可能触发的 树退化(untreeify) 机制,以及其与扩容、树化机制的关联。
  5. 源码阅读与底层实现原理的探究能力:通过此问题,可以侧面了解候选人是否深入研究过 JDK 源码,以及其理解复杂系统实现细节的能力。

核心答案

HashMap.remove(key) 方法的实现核心分为三步:

  1. 定位桶(Bucket):根据键(key)的哈希值,通过 (n - 1) & hash 计算其在底层数组(table)中的索引位置。
  2. 查找并删除节点:在定位到的桶中(可能是一个链表或一棵红黑树),遍历查找 key 相等的节点。找到后,根据节点所在数据结构是链表还是红黑树,执行相应的删除操作。
  3. 维护数据结构:删除节点后,更新相关引用。如果是红黑树,删除后可能触发 退化(untreeify) 操作,即当树根或其子节点满足特定条件(如根节点为空、根节点的右子节点或左子节点为空等)时,会将红黑树转换回链表,以节省空间。

该方法返回被删除键值对中的 value,如果 key 不存在则返回 null

深度解析

以下解析基于 JDK 17 的源码,核心逻辑与 JDK 8 及之后版本保持一致。

原理与流程

remove 方法的核心是 removeNode 方法。其执行流程如下:

  • 哈希计算与定位:首先对 key 进行哈希计算(调用 hash(key) 方法,内部是 (h = key.hashCode()) ^ (h >>> 16)),目的是将高比特位的影响扩散到低比特位,减少哈希冲突。然后用 hash & (table.length - 1) 得到数组下标。

  • 节点查找:定位到数组元素(第一个节点)。然后开始匹配,匹配条件包括:哈希值相等、key 引用相等或 equals 方法返回 true

  • 节点删除

    • 链表删除:如果要删除的是链表的头节点,直接将数组槽位指向头节点的下一个节点即可。否则,在遍历链表过程中,维护一个前驱节点(prev)的引用,找到目标节点后,执行 prev.next = node.next

    • 红黑树删除:调用红黑树专用的删除方法 TreeNode.removeTreeNode。这个方法会执行标准的红黑树删除逻辑,包括查找、删除节点以及必要的旋转和重新着色以保持红黑树平衡。

  • 结构维护:删除成功后,HashMapsize 减 1,并记录一次结构修改(modCount++)。对于树节点删除,在 removeTreeNode 内部会检查是否需要进行 退化。退化条件相对复杂,简单来说是当树的节点数太少(小于等于 UNTREEIFY_THRESHOLD,默认为 6)时,会将红黑树转换为链表,以优化内存使用。

代码示例(关键逻辑摘录)

// HashMap.remove(Object key) 的核心逻辑片段
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    // 1. 检查 table 不为空且长度>0,且计算出的索引位置有节点
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        // 2. 检查第一个节点是否就是目标
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            // 3. 如果该桶是树结构,则调用 getTreeNode 查找
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                // 4. 如果是链表结构,则遍历查找
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e; // p 在这里充当了前驱节点的角色
                } while ((e = e.next) != null);
            }
        }
        // 5. 找到节点后,执行删除
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode) // 树节点删除
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p) // 要删除的是链表头节点
                tab[index] = node.next;
            else // 要删除的是链表中间节点
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node); // 空方法,LinkedHashMap 等子类可扩展
            return node; // 返回被删除的节点
        }
    }
    return null; // 未找到,返回 null
}

最佳实践与常见误区

  • 键对象不可变:作为 HashMap 键的对象,其 hashCode()equals() 所依赖的属性必须是不可变的,否则在将其放入 HashMap 后修改这些属性,会导致无法通过 getremove 再次找到该对象,造成内存泄露。
  • 返回值判断remove(key) 返回 null 有两种可能:
    • 1)该 key 对应的 value 本来就是 null
    • 2)该 key 不存在。如果需要严格区分,应先用 containsKey 判断。
  • 并发场景HashMap 非线程安全。在迭代过程中直接调用 remove 方法(而非迭代器的 remove)会抛出 ConcurrentModificationException。多线程环境下的删除操作需要使用 ConcurrentHashMap 或进行外部同步。

总结

HashMap.remove 的本质是在其“数组-链表/红黑树”的复合结构中,精准定位并安全摘除目标节点,同时智能地维护底层数据结构(如树退化),是一个集哈希定位、数据结构操作与动态优化于一体的典型实现。