问题
有了equals为啥需要hashCode方法?
答案
核心原因
hashCode 是为哈希表(如HashMap、HashSet)提供高效查找而设计的。如果只有 equals 而没有 hashCode,哈希表的性能会从 O(1) 退化为 O(n)。
工作原理
1. HashMap的查找流程
HashMap<User, String> map = new HashMap<>();
User user = new User("张三", 25);
map.put(user, "员工信息");
// 查找过程
String value = map.get(new User("张三", 25));
内部执行流程:
1. 计算key的hashCode → hash = key.hashCode()
2. 根据hash定位数组索引 → index = hash & (table.length - 1)
3. 在该索引的链表/红黑树中遍历
4. 使用equals比较每个节点的key
5. 找到匹配的key,返回value
// HashMap.get()源码简化
public V get(Object key) {
// 步骤1: 计算hashCode
int hash = hash(key.hashCode());
// 步骤2: 定位数组索引
int index = hash & (table.length - 1);
// 步骤3: 遍历链表
Node<K,V> node = table[index];
while (node != null) {
// 步骤4: 使用equals比较
if (node.hash == hash &&
(node.key == key || key.equals(node.key))) {
return node.value; // 步骤5: 返回结果
}
node = node.next;
}
return null;
}
2. hashCode的作用
快速定位:将对象分散到不同的”桶”中,避免全局遍历。
假设HashMap有16个桶:
没有hashCode(只能放在同一个桶):
[0] → user1 → user2 → user3 → ... → user10000
[1]
[2]
...
查找复杂度:O(n)
有hashCode(分散到不同桶):
[0] → user1 → user5
[1] → user2
[2] → user3 → user9
...
查找复杂度:O(1)
问题演示
问题1:只重写equals,不重写hashCode
public class User {
private String name;
private int age;
// ✅ 重写了equals
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof User)) return false;
User other = (User) obj;
return age == other.age &&
Objects.equals(name, other.name);
}
// ❌ 没有重写hashCode(使用Object默认实现)
}
// 测试
User u1 = new User("张三", 25);
User u2 = new User("张三", 25);
System.out.println(u1.equals(u2)); // true ✅
// 放入HashMap
Map<User, String> map = new HashMap<>();
map.put(u1, "员工1");
// 使用逻辑相等的对象查询
String result = map.get(u2); // null ❌ 期望"员工1"
原因:
u1.hashCode() // 假设 12345
u2.hashCode() // 假设 67890(不同的内存地址)
// HashMap查找时:
// u2的hashCode定位到不同的桶 → 找不到u1存储的数据
问题2:HashSet去重失效
Set<User> set = new HashSet<>();
set.add(new User("张三", 25));
set.add(new User("张三", 25)); // 逻辑相等的对象
System.out.println(set.size()); // 2 ❌ 期望1
equals与hashCode契约
Java规范要求:
- 如果两个对象equals相等,则hashCode必须相等
if (obj1.equals(obj2)) { // 必须保证: obj1.hashCode() == obj2.hashCode() } - 如果两个对象hashCode相等,equals不一定相等(哈希冲突)
if (obj1.hashCode() == obj2.hashCode()) { // obj1.equals(obj2) 可能为true或false } - 不相等的对象,hashCode应该尽量不同(减少冲突)
正确实现
方式1:手动实现
public class User {
private String name;
private int age;
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof User)) return false;
User other = (User) obj;
return age == other.age &&
Objects.equals(name, other.name);
}
@Override
public int hashCode() {
// 使用Objects.hash计算多个字段的哈希值
return Objects.hash(name, age);
}
}
方式2:IDE自动生成
// IDEA: Alt + Insert → equals() and hashCode()
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
方式3:使用Lombok
@Data
@EqualsAndHashCode
public class User {
private String name;
private int age;
}
hashCode实现原则
1. 使用关键字段
public class Order {
private Long id; // 关键字段
private String orderNo; // 关键字段
private Date createTime; // 非关键字段
private BigDecimal amount; // 非关键字段
@Override
public int hashCode() {
// 只使用关键字段(唯一标识)
return Objects.hash(id, orderNo);
}
}
2. 保持一致性
// ❌ 错误:使用可变字段
public class User {
private String name; // 可变
@Override
public int hashCode() {
return Objects.hash(name); // name改变后hashCode也变
}
}
// 测试
User user = new User("张三");
map.put(user, "数据");
user.setName("李四"); // 修改name
map.get(user); // null ❌ 找不到了(hashCode变了)
建议:
- 不可变类:使用所有字段
- 可变类:只使用不变的标识字段(如ID)
3. 质数优化
// 经典实现(JDK源码风格)
@Override
public int hashCode() {
int result = 17; // 初始值用质数
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + age;
return result;
}
// 为什么用31?
// 1. 31是质数,减少哈希冲突
// 2. 31 * i = (i << 5) - i,JVM可优化为位运算
性能影响
糟糕的hashCode实现
// ❌ 最差:所有对象返回相同hashCode
@Override
public int hashCode() {
return 1; // 所有对象哈希冲突
}
// 影响:HashMap退化为链表,O(1) → O(n)
// ❌ 次差:使用常量
@Override
public int hashCode() {
return 100;
}
良好的hashCode实现
// ✅ 分布均匀
@Override
public int hashCode() {
return Objects.hash(id, name, age);
}
JMH性能测试:
// 100万次查询操作
Map<User, String> map = new HashMap<>();
// ... 插入10000个User对象
// 糟糕的hashCode(返回1)
// 查询时间:500 ms
// 良好的hashCode(Objects.hash)
// 查询时间:2 ms ← 快250倍
特殊场景
不可变类
@Immutable
public final class Point {
private final int x;
private final int y;
// 缓存hashCode(不可变对象可以这样优化)
private int hashCode;
@Override
public int hashCode() {
int result = hashCode;
if (result == 0) {
result = 17;
result = 31 * result + x;
result = 31 * result + y;
hashCode = result;
}
return result;
}
}
继承关系
public class Employee extends Person {
private String employeeId;
@Override
public boolean equals(Object obj) {
if (!super.equals(obj)) return false;
if (!(obj instanceof Employee)) return false;
Employee other = (Employee) obj;
return Objects.equals(employeeId, other.employeeId);
}
@Override
public int hashCode() {
// 包含父类的hashCode
return Objects.hash(super.hashCode(), employeeId);
}
}
常见错误
// ❌ 错误1:只重写equals
@Override
public boolean equals(Object obj) { ... }
// 缺少hashCode实现
// ❌ 错误2:equals和hashCode使用不同字段
@Override
public boolean equals(Object obj) {
return Objects.equals(this.name, other.name); // 只比较name
}
@Override
public int hashCode() {
return Objects.hash(name, age); // 包含age
}
// ❌ 错误3:hashCode使用随机数
@Override
public int hashCode() {
return new Random().nextInt(); // 每次调用结果不同
}
实战检查清单
// ✅ 完整实现示例
public class Product {
private Long id;
private String name;
private BigDecimal price;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Product product = (Product) o;
return Objects.equals(id, product.id) &&
Objects.equals(name, product.name) &&
Objects.equals(price, product.price);
}
@Override
public int hashCode() {
return Objects.hash(id, name, price);
}
}
// 验证
Product p1 = new Product(1L, "商品A", new BigDecimal("99.9"));
Product p2 = new Product(1L, "商品A", new BigDecimal("99.9"));
// 1. equals相等
assert p1.equals(p2); // ✅
// 2. hashCode相等
assert p1.hashCode() == p2.hashCode(); // ✅
// 3. HashMap正常工作
Map<Product, Integer> map = new HashMap<>();
map.put(p1, 100);
assert map.get(p2) == 100; // ✅
// 4. HashSet去重
Set<Product> set = new HashSet<>();
set.add(p1);
set.add(p2);
assert set.size() == 1; // ✅
答题总结
hashCode 用于哈希表(HashMap/HashSet)的快速定位,将对象分散到不同的桶中,实现O(1)查找。如果只重写 equals 而不重写 hashCode,会导致:
- HashMap/HashSet无法正确工作(查询失败、去重失效)
- 违反Java规范:equals相等的对象hashCode必须相等
最佳实践:
- 同时重写
equals和hashCode - 使用相同字段计算(推荐
Objects.hash()) - 避免使用可变字段或确保不修改
- IDE自动生成或使用Lombok简化代码