在 Java 中,Object
类的 hashCode()
和 equals(Object obj)
方法是两个非常关键的方法,它们在集合类(如 HashMap
、HashSet
、Hashtable
等)中起着核心作用。为了确保这些集合类的正确行为,重写 hashCode()
时必须遵循一定的规范,并且要与 equals()
方法保持一致性。
一、Object
类中默认行为
public boolean equals(Object obj) {
return (this == obj);
}
public native int hashCode();
equals()
:默认比较引用地址(即是否是同一个对象)。hashCode()
:默认返回一个基于对象内存地址的整数(由 JVM 实现,通常与内存地址相关)。
二、重写 hashCode()
的规范(来自官方文档)
根据 Java 官方文档,重写 hashCode()
必须遵守以下三条基本原则:
✅ 规范 1:一致性(Consistency)
在一个对象的生命周期内,只要用于
equals
比较的字段没有改变,hashCode()
必须始终返回相同的整数。
// 正确示例
public int hashCode() {
return Objects.hash(name, age); // 只依赖不变或稳定字段
}
⚠️ 注意:不要在
hashCode()
中使用可变字段(如lastModifiedTime
),否则会导致HashMap
中的对象“找不到”。
✅ 规范 2:相等对象必须有相同哈希码(Consistent with equals)
如果两个对象通过
equals(Object)
方法比较是相等的(即a.equals(b) == true
),那么它们的hashCode()
必须返回相同的值。
这是最重要的一条!否则会导致集合类(如 HashMap
)失效。
// ❌ 错误示例
public class Person {
private String name;
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person p = (Person) o;
return Objects.equals(name, p.name);
}
public int hashCode() {
return new Random().nextInt(); // 每次返回不同值 —— 严重错误!
}
}
即使
name
相同,hashCode()
不同 → 在HashSet
中可能被当作两个不同对象 → 重复添加!
✅ 规范 3:不相等对象尽量有不同的哈希码(Performance Recommendation)
如果两个对象不相等(
a.equals(b) == false
),它们的hashCode()
尽量返回不同的值,以减少哈希冲突,提高哈希表性能。
这不是强制要求,但影响性能。理想情况下,哈希函数应“均匀分布”。
三、hashCode()
与 equals()
的一致性原则
“相等的对象必须有相同的哈希码”
即:
如果a.equals(b) == true
,那么a.hashCode() == b.hashCode()
必须为 true。
🚨 反例:不一致的后果
Person p1 = new Person("Alice");
Person p2 = new Person("Alice");
System.out.println(p1.equals(p2)); // true(假设 name 相同即相等)
System.out.println(p1.hashCode()); // 123
System.out.println(p2.hashCode()); // 456(假设只对 age 做 hash)
Set<Person> set = new HashSet<>();
set.add(p1);
set.add(p2);
System.out.println(set.size()); // 输出 2!但逻辑上应为 1
❌ 问题:
HashSet
使用hashCode()
定位桶,再用equals()
判断是否重复。哈希码不同 → 放入不同桶 → 被当作两个对象!
四、如何正确重写?
✅ 推荐做法:使用 Objects.hash()
import java.util.Objects;
public class Person {
private String name;
private int age;
@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);
}
// toString() 建议也重写
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
✅ 手动实现(了解原理)
@Override
public int hashCode() {
int result = 17;
result = 31 * result + name.hashCode();
result = 31 * result + age;
return result;
}
- 初始值通常为 17(质数)
- 每个字段用 31 * result + fieldHash,31 是质数且易于 JVM 优化
五、注意事项
项目 | 建议 |
---|---|
🛑 不要使用可变字段 | 如 lastLoginTime ,否则对象放入 HashSet 后修改字段,可能导致无法查找 |
✅ 使用 @Override 注解 |
防止拼写错误,确保正确覆盖 |
✅ equals() 中先判断 null 和类型 |
避免 ClassCastException |
✅ 使用 IDE 或 Lombok 自动生成 | 如 IntelliJ IDEA 的 Generate > equals() and hashCode() ,或使用 @Data 注解(Lombok) |
// 使用 Lombok(简洁安全)
@Data
public class Person {
private String name;
private int age;
}
六、最佳实践总结
原则 | 说明 |
---|---|
✅ 同时重写 equals 和 hashCode |
两者必须“成对出现” |
✅ 保持一致性 | equals 相等 → hashCode 相等 |
✅ 基于不可变字段 | 保证哈希码稳定 |
✅ 使用 Objects.hash() |
简洁、安全、高效 |
✅ 避免性能陷阱 | 哈希冲突过多会退化为链表,影响性能 |
🔚 结论
只要重写了
equals()
,就必须重写hashCode()
,并且保证:
a.equals(b)
为true
时,a.hashCode() == b.hashCode()
也必须为true
。