hashCode()equals() 是 Java 中两个紧密关联的核心方法,它们共同定义了对象的相等性语义哈希行为。正确重写这两个方法对集合(尤其是 HashMapHashSet 等基于哈希的集合)的正确性和性能至关重要。


一、基本定义

1. equals(Object obj)

  • 作用:判断两个对象是否“逻辑相等”。
  • 默认实现(来自 Object 类):
    public boolean equals(Object obj) {
        return (this == obj); // 仅比较引用地址(即是否是同一个对象)
    }
    

2. hashCode()

  • 作用:返回对象的哈希码(一个 int 值),用于哈希表(如 HashMap)快速定位存储位置。
  • 默认实现(来自 Object 类):
    public native int hashCode(); // 通常基于对象内存地址生成(不同 JVM 可能不同)
    

二、两者之间的契约(Java 官方规范)

《Effective Java》第 3 章强调:只要重写 equals(),就必须重写 hashCode()

Java 规范明确规定:

  1. 一致性
    在对象未被修改影响 equals() 比较的前提下,多次调用 hashCode() 必须返回相同值。

  2. 相等性约束

    • 如果 a.equals(b) == true,那么 a.hashCode() == b.hashCode() 必须成立
    • 反之不成立:hashCode() 相等,equals() 不一定为 true(哈希冲突)。
  3. 不相等对象
    a.equals(b) == false 时,hashCode() 可以相同也可以不同(但不同可减少哈希冲突,提升性能)。

⚠️ 违反此契约会导致严重问题(见下文示例)。


三、为什么需要重写?—— 默认实现的问题

场景:自定义类用于 HashSetHashMap 的 key

class Person {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // ❌ 只重写 equals,不重写 hashCode
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person)) return false;
        Person p = (Person) o;
        return age == p.age && Objects.equals(name, p.name);
    }
}

问题演示:

Set<Person> set = new HashSet<>();
Person p1 = new Person("Alice", 20);
Person p2 = new Person("Alice", 20);

System.out.println(p1.equals(p2)); // true(逻辑相等)

set.add(p1);
set.add(p2);

System.out.println(set.size()); // 输出 2!❌ 应该是 1

原因分析:

  • p1p2 虽然 equals()true,但它们的 hashCode() 来自 Object 默认实现(基于内存地址),值不同
  • HashSet 内部使用 HashMap,先通过 hashCode() 定位桶(bucket),再用 equals() 比较。
  • 因为 hashCode() 不同,p1p2 被放入不同的桶equals() 根本不会被调用 → 重复元素被错误添加

💥 结论:不重写 hashCode() 会导致基于哈希的集合行为异常!


四、如何正确重写?

✅ 正确做法:同时重写 equals()hashCode()

import java.util.Objects;

class Person {
    private String name;
    private int age;

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

    @Override
    public int hashCode() {
        // 使用 Objects.hash 自动生成(推荐)
        return Objects.hash(name, age);
        
        // 或手动计算:
        // return name != null ? name.hashCode() * 31 + age : age;
    }
}

验证结果:

Set<Person> set = new HashSet<>();
set.add(new Person("Alice", 20));
set.add(new Person("Alice", 20));
System.out.println(set.size()); // 输出 1 ✅

五、重写原则与技巧

1. 参与 equals() 比较的字段,必须全部参与 hashCode() 计算

  • 否则可能违反“相等对象哈希码必须相同”的契约。

2. 使用 Objects.hash()(Java 7+)

@Override
public int hashCode() {
    return Objects.hash(field1, field2, field3);
}
  • 自动处理 null 值;
  • 使用标准算法(31 倍乘法)。

3. 不可变对象更安全

  • 如果对象在加入集合后被修改,且修改影响 hashCode(),会导致对象“丢失”(无法被找到)。
  • ✅ 建议:作为 Map 的 key 或 Set 元素的对象应为不可变对象(如 StringInteger)。

六、常见误区

误区 正确理解
“只要 equals() 返回 true 就行,hashCode() 无所谓” ❌ 违反契约,哈希集合失效
hashCode() 必须唯一” ❌ 允许冲突(只是影响性能)
equals()== 比较就行” ❌ 逻辑相等 ≠ 引用相等
“基本类型不用考虑” ❌ 包装类(如 Integer)已正确重写,但自定义类必须自己处理

七、总结:为什么要重写?

场景 是否需要重写
对象仅用于局部变量、临时计算 ❌ 不需要
对象要放入 HashSetHashMapHashtable 必须同时重写 equals()hashCode()
对象要作为 Map 的 key 必须重写
对象要进行逻辑相等比较(而非引用比较) ✅ 至少重写 equals(),但通常也需要 hashCode()

黄金法则
“若重写 equals(),必重写 hashCode();反之,若不重写 equals(),通常也不应重写 hashCode()。”

正确实现这两个方法,是写出健壮、高效 Java 程序的基础。现代 IDE(如 IntelliJ IDEA、Eclipse)均可自动生成符合规范的代码,但理解其原理至关重要。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐