HashSet去重依赖hashCode与equals协同:先用hashCode定位桶,再用equals桶内判等;若二者未同步重写或字段不一致,会导致去重失效。

HashSet 本身不直接“结合”equals方法工作,而是通过底层 HashMap 的键存储机制,依赖对象的 hashCode 和 equals 协同判断唯一性。避免逻辑漏洞的关键,不是单独写好 equals,而是让这两个方法严格遵守契约、字段一致、行为稳定。
必须同时重写 hashCode,且字段完全对齐
只重写 equals 是最常见也最危险的漏洞。HashSet 添加元素时,先用 hashCode 定位桶(数组索引),再在该桶内遍历调用 equals 判等。如果两个逻辑相等的对象 hashCode 不同,它们根本不会进同一个桶,equals 就永远不会被调用——等于“没看见对方”,自然无法去重。
- equals 中比较了 id 和 amount,hashCode 就必须也只用 id 和 amount 计算
- 推荐统一用 Objects.hash(id, amount),避免手写 31 * x + y 出错或遗漏
- 禁止在 hashCode 中使用 name 等可能后续修改的字段,否则对象加入 HashSet 后改 name,哈希值变,原位置找不到了
equals 要满足五项基本契约,不能只比字段
手写 equals 光写 return this.id == that.id 是不够的。JVM 和集合框架会基于这些数学性质做优化和假设,违反任一契约都可能引发不可预测行为,比如 Set.contains 返回 false、remove 失败、甚至并发场景下死循环。
Java Maven 项目审查工作流的通用输入处理。当 Java Maven 项目以 ZIP压缩包或 GitLab 仓库 URL 形式提供且需要...时使用。
-
自反性:
a.equals(a)必须为 true(空指针检查后第一行就该 return true) -
对称性:若
a.equals(b)为 true,则b.equals(a)也得 true(类型检查要用getClass() == o.getClass(),不用 instanceof) - 一致性:只要参与比较的字段不变,多次调用结果不能变(别在 equals 里查数据库或调用随机函数)
-
null 安全:任何对象调用
x.equals(null)都必须返回 false,不能抛 NPE(Objects.equals 已帮你处理) - 传递性:若 a=b 且 b=c,则 a=c —— 这要求字段比较逻辑无歧义,比如避免用浮点数直接 == 比较
对象一旦放进 HashSet,影响 hashCode 的字段就不能再改
HashSet 不会监听对象内部变化。一个 Person 对象以 id=1001 存入后,如果后续执行 p.setId(2002),它的 hashCode 立刻改变,但 HashSet 仍认为它在原来的桶里。此时 set.contains(p) 或 set.remove(p) 都会失败——因为新哈希值指向另一个桶,而旧桶里已找不到这个“新身份”的 p。
立即学习“Java免费学习笔记(深入)”;
- 解决方案一:把参与判等的字段设计为 final(如 id),从源头杜绝修改
- 解决方案二:若业务必须可变,操作前先
set.remove(p),再改字段,最后set.add(p) - 切勿依赖 setter 修改后集合自动同步——它不会
优先用工具生成,但要懂原理再调整
IDE 自动生成或 Lombok 的 @EqualsAndHashCode 能大幅减少低级错误,但默认策略未必贴合业务。
- IntelliJ 默认可能包含所有非 static 字段,包括 createTime、status 等不该参与判等的字段
- Lombok 的
callSuper = true可能引入父类未预期的字段,破坏契约 - 生成后务必检查:哪些字段进了 equals?是否也都进了 hashCode?有没有 null 安全处理?

















