equals和hashCode的正确理解

equals和hashCode的正确理解

equals()hashCode() 是 Java 对象比较的两个核心方法,它们的正确实现直接影响集合类的行为。本文从契约、实现到最佳实践进行全面梳理。

Object 的默认实现

// 默认比较内存地址
public boolean equals(Object obj) {
return (this == obj);
}

// 默认返回对象地址的哈希值
public native int hashCode();

equals 重写规范

根据 Java 规范,equals() 必须满足以下特性:

  1. 自反性x.equals(x) 必须为 true
  2. 对称性x.equals(y) == y.equals(x)
  3. 传递性x.equals(y) && y.equals(z)x.equals(z)
  4. 一致性:多次调用结果不变
  5. 非空性x.equals(null) 必须为 false

hashCode 重写规范

  1. 一致性:同一对象多次调用返回相同值
  2. 相等性equals() 相等的对象,hashCode() 必须相等
  3. 不相等性hashCode() 不相等的对象,equals() 一定不相等

为什么必须同时重写

如果只重写 equals,不重写 hashCode:

public class Person {
private String name;
// 只重写了equals,没重写hashCode
}

Person p1 = new Person("张三");
Person p2 = new Person("张三");

Set<Person> set = new HashSet<>();
set.add(p1);
set.add(p2);

System.out.println(set.size()); // 输出2!期望是1

HashSet 先比较 hashCode,再比较 equals。hashCode 不同会直接认为是不同对象。

IntelliJ IDEA 自动生成

现代 IDE 可以自动生成符合规范的方法:

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}

@Override
public int hashCode() {
return Objects.hash(name, age);
}

使用 Objects.equals 避免 NPE

// 可能抛出NullPointerException
if (str1.equals(str2)) { ... }

// 安全写法
if (Objects.equals(str1, str2)) { ... }

可变对象的陷阱

永远不要对可变对象使用自定义 equals/hashCode 作为 HashMap 的 Key:

public class BadKey {
private int value;
// equals和hashCode基于value

// 放入Map后修改value
map.put(key, "data");
key.setValue(100); // hashCode变了!

map.get(key); // 可能找不到!
}

解决方案:使用不可变对象作为 Key,如 String、Integer、自定义不可变类。

Lombok 简化实现

@Data
public class User {
private Long id;
private String name;
}
// 自动生成equals、hashCode、toString等

或使用 @EqualsAndHashCode

@EqualsAndHashCode(of = {"id", "name"})
public class User { ... }

性能优化

缓存 hashCode

对于频繁使用且计算成本高的对象:

public class CachedHashCode {
private int hashCode;
private boolean hashCodeCalculated = false;

@Override
public int hashCode() {
if (!hashCodeCalculated) {
hashCode = Objects.hash(field1, field2);
hashCodeCalculated = true;
}
return hashCode;
}
}

选择关键字段

equals 比较应该使用能够唯一标识对象的字段

// 好:使用业务唯一键
@EqualsAndHashCode(of = "userId")

// 差:包含所有字段,包括可能变化的字段
@EqualsAndHashCode(of = {"userId", "lastLoginTime", "status"})

最佳实践检查清单

  • 同时重写 equals 和 hashCode
  • 使用 IDE 自动生成而非手写
  • 使用 Objects.equals 进行空安全比较
  • 避免在可变对象上依赖 hashCode
  • 选择最小唯一字段集作为比较依据
  • 对于实体类,通常只用 id 字段

总结

equalshashCode 的契约是 Java 集合框架正确工作的基石。理解并正确实现这两个方法,是每一位 Java 开发者的基本功。


   转载规则


《equals和hashCode的正确理解》 小乐 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录