跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava算法

Java equals() 与 hashCode() 契约详解及容器类(List/Set/Map)核心整理

Java 中 equals() 与 hashCode() 的共生契约关系,阐述违反契约在 HashMap 中导致的 Bug 原理。介绍了 HashMap 判断对象相等的“先哈希码后 equals”双重机制及其设计原因。同时系统整理了 List、Set、Map 三大接口及其常见实现类(如 ArrayList、HashSet、HashMap 等)的底层数据结构、线程安全性及适用场景,提供快速选择指南。

雾岛听风发布于 2026/3/30更新于 2026/10/25113 浏览
Java equals() 与 hashCode() 契约详解及容器类(List/Set/Map)核心整理

经典问题

  1. 为什么重写 equals() 一定要重写 hashcode?
  2. HashMap 是怎么判断两个对象是否相等的?
  3. HashMap 为什么要这么做?

一、为什么重写 equals() 一定要重写 hashCode()?

1. Java 官方的硬性契约(必须遵守)

java.lang.Object 类中明确规定了两个方法的共生关系:

  • 如果两个对象通过 equals() 比较相等,那么它们的 hashCode()必须返回相同的整数。
  • 如果两个对象通过 equals() 比较不相等,它们的 hashCode()可以相同也可以不同(但建议不同,以提高哈希表性能)。
2. 违反契约的后果(以 HashMap 为例)

如果你只重写了 equals() 而没重写 hashCode(),会导致哈希集合(HashMap、HashSet、HashTable 等)逻辑混乱:

  • 场景:你创建了两个对象 A 和 B,A.equals(B) == true,但 A.hashCode() != B.hashCode()。
  • 存入 HashMap:map.put(A, "value") 会根据 A 的哈希码找到一个位置存入。
  • 读取 HashMap:map.get(B) 会根据 B 的哈希码去找(可能找到另一个位置),结果找不到(认为是两个不同的 Key),尽管逻辑上它们是相等的。

二、HashMap 是怎么判断两个对象(Key)是否相等的?

HashMap 判断两个 Key 是否相等,遵循**'先哈希码,后 equals'**的双重检查机制:

  1. 第一步:比较 hashCode()
    1. 先调用 Key 对象的 hashCode() 方法,计算哈希值。
    2. 如果哈希值不同:直接判定两个对象不相等(连 equals 都不用比了,效率高)。
    3. 如果哈希值相同(哈希碰撞):进入第二步。
  2. 第二步:比较 equals()
    1. 调用 Key 对象的 equals() 方法进行内容比对。
    2. 如果 equals() 返回 true:判定两个对象相等(视为同一个 Key)。
    3. 如果 equals() 返回 false:判定两个对象不相等(虽然哈希冲突,但仍是不同 Key)。

三、HashMap 为什么要这么设计?

这是为了兼顾性能与准确性,本质是由 HashMap 底层的**'哈希表(数组 + 链表/红黑树)'**数据结构决定的:

1. 为什么要先比 hashCode()?(为了快)
  • HashMap 的核心优势是查询速度极快(接近 O(1))。
  • 它通过 hashCode 直接计算出 Key 在数组中的存储下标,从而快速定位。
  • 如果不先比 hashCode,而是每次都遍历所有元素调用 equals,那性能就退化成了链表(O(n)),失去了哈希表的意义。
2. 为什么还要比 equals()?(为了准)
  • :不同的对象可能算出相同的 hashCode(就像不同的人可能有相同的指纹概率)。
哈希碰撞是不可避免的
  • 因此,hashCode 相同只能说明它们应该放在数组的同一个'篮子'(链表/红黑树)里,但不能说明它们是同一个对象。
  • 必须通过 equals() 进行最终的内容确认,才能保证逻辑的绝对正确性。
  • 总结

    • hashCode 决定了对象在哈希表中**'坐哪一排'**(快速定位)。
    • equals 决定了对象在这一排里**'是不是同一个人'**(精确匹配)。
    • 两者缺一不可,所以重写 equals 必须重写 hashCode。

    List 接口(有序、可重复,按索引访问)

    实现类底层数据结构核心特色
    ArrayList动态数组1. 随机访问(索引)效率 O(1),增删需移动元素(效率 O(n));
    2. 线程不安全,适合查询多、增删少的场景。
    LinkedList双向链表(JDK1.7+ 取消循环)1. 增删只需修改节点指针(首尾操作 O(1)),随机访问需遍历(O(n));
    2. 实现了 Deque 接口,可作为队列/栈使用;
    3. 线程不安全,适合增删多、查询少的场景。
    Vector动态数组1. 与 ArrayList 类似,但所有方法加 synchronized 修饰;
    2. 线程安全但效率低,已很少使用。
    Stack继承 Vector,动态数组1. 后进先出(LIFO);
    2. 线程安全但效率低,推荐用 Deque(如 LinkedList)替代。

    Set 接口(无序、不可重复,依赖 equals()+hashCode())

    实现类底层数据结构核心特色
    HashSet基于 HashMap(存储在 key 中)1. 哈希表(数组 + 链表 + 红黑树,JDK1.8+)实现;
    2. 无序,添加/查询/删除效率平均 O(1);
    3. 线程不安全,需重写 equals() 和 hashCode() 保证唯一性。
    LinkedHashSet继承 HashSet,基于 LinkedHashMap1. 哈希表 + 双向链表实现;
    2. 有序(按插入顺序或访问顺序);
    3. 性能略低于 HashSet(需维护链表),线程不安全。
    TreeSet基于 TreeMap(红黑树)1. 红黑树(自平衡二叉搜索树)实现;
    2. 有序(自然排序或定制排序,需实现 Comparable 接口);
    3. 增删改查效率 O(logn),线程不安全。

    Map 接口(键值对存储,key 不可重复,value 可重复)

    实现类底层数据结构核心特色
    HashMap哈希表(数组 + 链表 + 红黑树)1. JDK1.8+:链表长度≥8 且数组长度≥64 时转红黑树;
    2. 无序,允许1 个 null 键和多个 null 值;
    3. 线程不安全,多线程需用 ConcurrentHashMap 替代。
    LinkedHashMap继承 HashMap,哈希表 + 双向链表1. 维护插入顺序或访问顺序(accessOrder=true 时为 LRU 缓存);
    2. 性能略低于 HashMap,线程不安全。
    TreeMap红黑树1. 按 key自然排序或定制排序;
    2. 不允许 null 键(但允许 null 值);
    3. 增删改查效率 O(logn),线程不安全。
    Hashtable哈希表(数组 + 链表,无红黑树)1. 所有方法加 synchronized 修饰;
    2. 线程安全但效率低,不允许 null 键/值;
    3. 已过时,推荐用 ConcurrentHashMap。
    ConcurrentHashMap哈希表(数组 + 链表 + 红黑树)1. JDK1.8+:用 CAS+synchronized(只锁链表/红黑树头节点)替代分段锁;
    2. 线程安全,效率远高于 Hashtable;
    3. 不允许 null 键/值,适合高并发场景。

    快速选择指南

    • List:选 ArrayList(查询多)或 LinkedList(增删多/队列)。
    • Set:选 HashSet(通用)、LinkedHashSet(需有序)、TreeSet(需排序)。
    • Map:选 HashMap(通用)、LinkedHashMap(需有序)、TreeMap(需排序)、ConcurrentHashMap(高并发)。

    目录

    1. 经典问题
    2. 一、为什么重写 equals() 一定要重写 hashCode()?
    3. 1. Java 官方的硬性契约(必须遵守)
    4. 2. 违反契约的后果(以 HashMap 为例)
    5. 二、HashMap 是怎么判断两个对象(Key)是否相等的?
    6. 三、HashMap 为什么要这么设计?
    7. 1. 为什么要先比 hashCode()?(为了快)
    8. 2. 为什么还要比 equals()?(为了准)
    9. 总结
    10. List 接口(有序、可重复,按索引访问)
    11. Set 接口(无序、不可重复,依赖 equals()+hashCode())
    12. Map 接口(键值对存储,key 不可重复,value 可重复)
    13. 快速选择指南

    更多推荐文章

    查看全部
    • Linux 进程信号入门:软中断机制与实战
    • 存储扇区分配表:NAND Flash 与 SD NAND 架构差异
    • Web Bluetooth API 实战指南:从设备连接到数据交互
    • DeerFlow 2.0 开源:字节跳动超级智能体架构详解
    • CentOS 安装 Python 3.12 实战记录
    • SD-XL 1.0 Refiner 图像优化工具使用指南
    • PyTorch 强化学习实战:策略梯度算法详解
    • 无人机 RemoteID 合规实战:基于 ArduRemoteID 的开源方案解析
    • Python OCR 技术入门与验证码识别实战
    • 基于迁移学习的个性化 AKI 预测模型开发与验证
    • OpenClaw 新手指南:环境搭建、模型配置与远程访问
    • 大模型时代人形机器人感知:视觉 - 语言模型应用
    • Coze 智能体核心资源详解:插件、知识库与数据库实战
    • π0 源码剖析:基于 PaLI-Gemma 的扩散策略与 C/S 架构部署
    • IPSG 静态绑定配置:IP 与 MAC 地址绑定
    • 程序员为何需要了解 GPT 并掌握 AI 大模型构建技术
    • 前端开发核心基础:HTML、CSS 与 JavaScript 实战入门
    • 环境模型在 DQN 中的应用:预测与规划机制解析
    • Apache SeaTunnel Web 部署及可视化数据集成实践
    • 大模型落地困境分析与解决思路

    相关免费在线工具

    • Keycode 信息

      查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

    • Escape 与 Native 编解码

      JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

    • JavaScript / HTML 格式化

      使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

    • JavaScript 压缩与混淆

      Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

    • 加密/解密文本

      使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

    • Gemini 图片去水印

      基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online