重载 equals 时必须同时重载 hashCode,确保对称性、一致性等契约,并将所有参与 equals 比较的字段纳入 hashCode 计算;否则 HashSet 查找会失败。

重载 equals 时不遵守对称性会导致 HashSet 查找不到对象
Java 中 HashSet 依赖 hashCode 定位桶、再用 equals 精确比对。如果 equals 返回 true 但 hashCode 不一致,对象可能被插入到错误桶中,后续 contains 或 remove 就会失败。
常见错误是只比较子类字段却忽略父类字段,或在 equals 中做了非对称判断(比如 if (obj instanceof SubClass) 而不处理反向情况)。
- 必须同时重载
equals和hashCode,缺一不可 -
equals必须满足自反性、对称性、传递性、一致性、对 null 的处理(推荐用Objects.equals(a, b)) - 所有参与
equals比较的字段,也必须参与hashCode计算 - 建议用
Objects.hash(field1, field2, ...)生成hashCode,避免手写乘加逻辑出错
hashCode 返回常量(如 return 1;)会让 HashSet 退化成链表
虽然技术上合法,但所有对象都落在同一个桶里,HashSet 的 O(1) 查找退化为 O(n),尤其在数据量大时性能骤降。
更隐蔽的问题是:某些 JVM 或 JDK 版本会对连续相同 hashCode 做特殊处理(如转红黑树阈值提前),反而引发意料外的行为。
- 不要为了“省事”返回固定值
- 字段值为
null时,Objects.hash(...)自动按 0 处理,安全 - 若字段是数组,需用
Arrays.hashCode(arr),不能直接用arr.hashCode()(那是引用哈希) - 浮点字段慎用
Double.doubleToLongBits转换后再哈希,避免NaN导致不等价对象哈希一致
使用 Lombok 的 @EqualsAndHashCode 时漏掉关键字段
Lombok 默认只包含非静态、非瞬态(non-transient)字段,但如果你有业务上决定唯一性的字段被标记为 transient,或用了 @JsonIgnore 等注解干扰了字段识别,生成的 equals/hashCode 就会漏掉它。
典型场景:DTO 转实体时,ID 字段从数据库回填,但被设为 transient 防止序列化;结果两个不同 ID 的对象在 HashSet 中被视为相等。
- 显式声明要包含的字段:
@EqualsAndHashCode(onlyExplicitlyIncluded = true)+ 在字段上加@EqualsAndHashCode.Include - 检查生成的字节码或反编译代码,确认字段是否真被纳入
- 若类继承自父类且父类有状态字段,需用
callSuper = true,否则父类字段被忽略
修改已加入 HashSet 的对象字段导致哈希桶错位
这是最易被忽略的陷阱:HashSet 不监控对象内部变化。一旦对象插入后,其参与 hashCode 计算的字段被修改,该对象就“永远留在原桶里”,后续 contains 会去新哈希值对应的桶找,自然找不到。
例如:一个 User 对象以 name 和 age 构建哈希,加入集合后改了 age,再调 set.contains(user) 就返回 false,哪怕对象本身没变。
- 把参与
hashCode/equals的字段设为final是最彻底的解决方式 - 若必须可变,操作前先
remove,改完再add,但要注意并发场景下的竞态 - 文档里必须明确标注哪些字段影响集合行为,避免下游误用
真正麻烦的不是写错 equals,而是它在多数测试路径下看似正常,只在特定哈希冲突或字段修改后才暴露问题。










