说说hashCode() 和 equals() 之间的关系

前言

在 Java 中,hashCode()equals() 是 Object 类的两个核心方法,二者紧密配合,尤其在哈希集合(如 HashMap、HashSet)中发挥关键作用。理解它们的关系,是写出高效、无 Bug 代码的基础。

一、先明确:两者的"本职工作"

在讲关系前,需先清楚各自的作用------它们的设计初衷就决定了必须协同工作。

二、核心关系:三大"黄金法则"

这是 Java 官方定义的强制约定,违反则会导致哈希集合(如 HashMap)出现逻辑错误:

  1. 法则1:若 a.equals(b) == true,则 a.hashCode() == b.hashCode() 必须成立

    哈希集合的工作逻辑是"先找桶,再比对象":先通过 hashCode() 定位对象所在的哈希桶,再在桶内用 equals() 逐个对比。若相等的对象哈希值不同,会被分到不同桶中,导致哈希集合认为"这是两个不同对象",出现存不进、查不到的 Bug。

  2. 法则2:若 a.hashCode() == b.hashCode()a.equals(b) 不一定为 true

    哈希值是 int 类型(范围 -2³¹ ~ 2³¹-1),而对象的数量远超过这个范围,必然会出现"哈希碰撞"(不同对象哈希值相同)。此时需要通过 equals() 进一步判断对象是否真的相等。

  3. 法则3:若 a.equals(b) == falsea.hashCode()b.hashCode() 可同可不同

    不强制要求不等的对象哈希值不同,但尽量让它们不同------若大量不等的对象哈希值相同,会导致哈希桶内元素过多,查询效率从 O(1) 退化到 O(n)。

三、反例:违反约定会导致什么问题?

假设我们自定义一个 User 类,只重写 equals() 但不重写 hashCode(),看看在 HashMap 中会出现什么问题:

java 复制代码
class User {
    private Integer id;
    private String name;

    // 构造器、getter、setter 省略
    public User(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    // 只重写 equals():id 相同则认为相等
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(id, user.id);
    }

    // 未重写 hashCode(),使用 Object 类的默认实现
    // Object.hashCode() 返回对象的内存地址相关值,不同对象哈希值不同
}

// 测试代码
public class HashCodeEqualsTest {
    public static void main(String[] args) {
        User u1 = new User(1, "张三");
        User u2 = new User(1, "张三");

        // 1. equals() 结果:true(因为 id 相同)
        System.out.println("u1.equals(u2) = " + u1.equals(u2)); // 输出 true

        // 2. hashCode() 结果:不同(默认实现依赖内存地址)
        System.out.println("u1.hashCode() = " + u1.hashCode()); // 例如:1163157884
        System.out.println("u2.hashCode() = " + u2.hashCode()); // 例如:1956725890

        // 3. 放入 HashMap,出现 Bug!
        Map<User, String> userMap = new HashMap<>();
        userMap.put(u1, "张三的信息");
        
        // 期望:能通过 u2 查到值,但实际返回 null
        System.out.println("userMap.get(u2) = " + userMap.get(u2)); // 输出 null
    }
}

问题原因
u1u2equals() 为 true,但 hashCode() 不同。HashMap 存储 u1 时,通过 u1.hashCode() 定位到桶 A;查询 u2 时,通过 u2.hashCode() 定位到桶 B------两个不同的桶,自然查不到 u1 的值,违背了"逻辑相等的对象应被视为同一个键"的预期。

四、正确实践:重写 equals() 必须同时重写 hashCode()

遵循"相等的对象必须有相等的哈希值",重写时需保证:equals() 中用到的字段,必须全部参与 hashCode() 的计算

User 类为例,正确重写如下:

java 复制代码
import java.util.Objects;
import java.util.HashMap;
import java.util.Map;

class User {
    private Integer id;
    private String name;

    public User(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    // 重写 equals():id 和 name 都相同,才认为相等
    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // 先判断内存地址(快速短路)
        if (o == null || getClass() != o.getClass()) return false; // 非空、同类型校验
        User user = (User) o;
        // equals() 依赖的字段:id 和 name
        return Objects.equals(id, user.id) && Objects.equals(name, user.name);
    }

    // 重写 hashCode():必须包含 equals() 中用到的所有字段(id 和 name)
    @Override
    public int hashCode() {
        // Objects.hash() 会自动处理 null,避免空指针
        return Objects.hash(id, name);
    }

    // getter 省略(测试用)
    public Integer getId() { return id; }
    public String getName() { return name; }
}

// 测试:正确工作
public class CorrectTest {
    public static void main(String[] args) {
        User u1 = new User(1, "张三");
        User u2 = new User(1, "张三");

        // equals() 为 true,hashCode() 也相等
        System.out.println("u1.equals(u2) = " + u1.equals(u2)); // true
        System.out.println("u1.hashCode() = " + u1.hashCode()); // 例如:68532510
        System.out.println("u2.hashCode() = " + u2.hashCode()); // 例如:68532510

        // HashMap 能正确查询
        Map<User, String> userMap = new HashMap<>();
        userMap.put(u1, "张三的信息");
        System.out.println("userMap.get(u2) = " + userMap.get(u2)); // 输出:张三的信息
    }
}

关键技巧

使用 Objects.hash(字段1, 字段2, ...) 重写 hashCode(),既简洁又能避免 null 指针(若字段为 null,Objects.hash() 会将其哈希值视为 0)。

五、总结

  1. 核心关系equals() 是"逻辑相等"的最终判断,hashCode() 是"快速定位"的辅助;相等的对象必须有相等的哈希值,哈希值相等的对象不一定相等。
  2. 必守原则 :重写 equals() 时,必须同步重写 hashCode(),且两者依赖的字段必须完全一致。
  3. 应用场景 :在使用 HashMap、HashSet、HashTable 等哈希集合时,若自定义对象作为键(或元素),务必保证 equals()hashCode() 正确重写,否则会出现逻辑错误。

记住:两者的设计是"分工协作",而非"各自独立"------理解这一点,才能避免 Java 开发中最常见的哈希集合 Bug。

相关推荐
若鱼19191 小时前
Kafka如何配置生产者拦截器和消费者拦截器
java·kafka
渣哥1 小时前
Java 自适应自旋锁机制详解:原理、优缺点与应用场景
java
花果山最Man的男人2 小时前
@Autowired注解使用说明
后端
京东云开发者2 小时前
如何秒级实现接口间“幂等”补偿:一款轻量级仿幂等数据校正处理辅助工具
后端
摇滚侠2 小时前
java语言中,list<String>转成字符串,逗号分割;List<Integer>转字符串,逗号分割
java·windows·list
烽学长2 小时前
(附源码)基于Spring Boot的宿舍管理系统设计
java
lssjzmn2 小时前
基于Spring Boot与Micrometer的系统参数监控指南
java·spring boot·数据可视化
柯南二号2 小时前
【Java后端】Spring Boot 集成雪花算法唯一 ID
java·linux·服务器