去年在金融项目里处理一笔对账数据时,我们突然发现系统漏掉了300多条记录------明明两个Transaction对象的金额和账户ID完全一致,HashSet.contains()却始终返回false。排查到最后,问题竟然出在一个看似简单的equals方法上。你是不是也以为重写equals就是比较几个字段?今天咱们就聊聊那些年equals方法里藏过的坑。
当equals遇上HashSet:一个对账系统的惨案
我们的对账模块需要比对两个数据源的交易记录,逻辑很简单:用HashSet存储基准数据,然后遍历待核对数据调用contains()比对。在测试环境一切正常,但生产环境跑完总有漏网之鱼。
通过日志最终锁定问题:某些Transaction对象虽然业务字段值相同,但hashCode()返回值却不一样------因为有人偷懒只重写了equals却忘了hashCode。这里有个硬规则:如果两个对象equals返回true,它们的hashCode必须相同。否则,当对象作为键存入HashMap或HashSet时,会存在哈希桶定位错误的风险。
java
// 错误示范:只重写equals
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Transaction that = (Transaction) o;
return amount.equals(that.amount) &&
accountId.equals(that.accountId);
}
// 正确姿势:必须配套重写hashCode
@Override
public int hashCode() {
return Objects.hash(amount, accountId); // 使用相同字段
}
为什么Lombok的@EqualsAndHashCode也翻过车
你可能会说:"我用Lombok自动生成不就完了?"别急,去年另一个坑正出在这里。某次上线后,监控突然显示某个核心服务CPU飙到90%,堆栈显示卡在HashMap.get()------因为自动生成的hashCode()包含了全部字段,而其中一个字段是BLOB类型的大文本!
- 关键点:hashCode()必须满足一致性(对象不变时返回值不变),但不要求所有字段参与运算*。对于重型字段,应该手动排除:
java
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Order {
@EqualsAndHashCode.Include
private Long id; // 只使用轻量级字段
private byte[] pdfContent; // 排除大字段
}
实测对比:包含10KB文本字段的类,调用hashCode()耗时从1200ns降至60ns(JMH基准测试,预热后结果)。
谁杀死了你的equals性能?
假设你给用户权限系统写了这样的equals:
java
// 性能杀手:无谓的字符串比对
@Override
public boolean equals(Object o) {
//...省略判空
User user = (User) o;
return username.equals(user.username) &&
permissions.stream()
.sorted()
.collect(joining(","))
.equals(user.permissions.stream()
.sorted()
.collect(joining(",")));
}
问题出在每次比较都要对流进行排序和拼接。如果权限列表有20项,单次equals耗时直接突破1ms(实测数据)。对于高频调用的场景,这种写法就是自杀。优化原则:优先比较大概率不等的字段,避免深层嵌套结构的完全遍历:
java
return username.equals(user.username) &&
permissions.size() == user.permissions.size() &&
permissions.containsAll(user.permissions); // 前提是Set实现
避坑清单:equals重写的军规
- hashCode必须和equals同步重写:违反这条直接导致HashMap/HashSet行为异常
- 避免可变字段参与计算:如果参与equals的字段会被修改,对象作为Map键时将"失踪"
- 数组字段用Arrays.equals比较:直接调用array.equals()比较的是引用
- 浮点数字段用Double.compare:处理NaN和±0.0等特殊情况
- 子类equals必须满足里氏替换原则:子类新增字段会导致对称性被破坏
终极答案:什么时候才该重写equals?
经过这些年踩坑,我的判断标准只有两条:
- 需要对象逻辑相等而非引用相等时(比如值对象)
- 确定该对象会作为集合元素 或Map键使用时
否则,直接使用Object的默认实现反而是最安全的选择。你在项目里遇到过哪些equals的奇葩坑?欢迎分享你的血泪史。