ZetCode

Java equals 和 hashCode

上次修改:2025 年 5 月 7 日

equalshashCode 方法在 Java 的对象比较和哈希机制中起着至关重要的作用。 一个良好实现的 equals 方法确保了准确的对象相等性检查,而一个优化的 hashCode 方法使得在基于哈希的集合(如 HashMapHashSet)中高效地使用对象成为可能。 正确重写这些方法对于在 Java 应用程序中维护一致且可预测的行为至关重要。

这些方法最初定义在 Object 类中,它是 Java 类层次结构的根。 由于所有 Java 对象默认继承这些方法,因此理解它们的行为至关重要。 默认情况下,equals 检查引用相等性(即两个对象引用是否指向同一内存地址),而 hashCode 根据对象的内存位置生成唯一标识符。

然而,对于自定义类,通常需要重写这些方法,以便根据对象的状态(例如属性值)而不是引用标识提供有意义的相等性比较。 重写 equals 时,请确保与 hashCode 一致——根据 equals 认为相等的对象必须返回相同的哈希码。 这种一致性可防止基于哈希的集合中出现意外行为,并确保高效的数据检索。

equals 方法

equals 方法用于比较两个对象是否相等。Object 类中的默认实现提供引用相等性(使用 == 运算符)。大多数类应该重写此方法以提供基于值的相等性,比较对象的实际内容。

让我们定义一个 Person 类,我们将在示例中使用它。 它包括姓名、年龄和护照号码等字段,以及正确实现的 equalshashCode 方法。

Person.java
package com.zetcode;

import java.util.Objects;

public class Person {

    private String name;
    private int age;
    private String passportNumber;

    public Person(String name, int age, String passportNumber) {
        this.name = name;
        this.age = age;
        this.passportNumber = passportNumber;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getPassportNumber() {
        return passportNumber;
    }

    @Override
    public boolean equals(Object o) {
        // 1. Check if the same object instance
        if (this == o) return true;
        
        // 2. Check if null or different class
        if (o == null || getClass() != o.getClass()) return false;
        
        // 3. Cast to the correct type and compare significant fields
        Person person = (Person) o;
        return age == person.age &&
               Objects.equals(name, person.name) &&
               Objects.equals(passportNumber, person.passportNumber);
    }

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

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + 
               ", passportNumber='" + passportNumber + "'}";
    }
}

以下 EqualsDemo 类演示了我们 Person 类中 equals 方法的用法。

EqualsDemo.java
package com.zetcode;

public class EqualsDemo {

    public static void main(String[] args) {

        // Assumes Person.java is compiled and in the classpath,
        // or in the same project/package.
        Person p1 = new Person("John Doe", 30, "A123456");
        Person p2 = new Person("John Doe", 30, "A123456"); // Same data as p1
        Person p3 = new Person("Jane Smith", 25, "B654321"); // Different data
        
        System.out.println("p1: " + p1);
        System.out.println("p2: " + p2);
        System.out.println("p3: " + p3);

        System.out.println("p1 equals p2: " + p1.equals(p2)); // true
        System.out.println("p1 equals p3: " + p1.equals(p3)); // false
        System.out.println("p1 equals null: " + p1.equals(null)); // false

        // Comparing with an object of a different type
        String s = "A string object";
        System.out.println("p1 equals String object: " + p1.equals(s)); // false
    }
}

此示例演示了 Person 类中正确的 equals 实现。 EqualsDemo 展示了它如何根据其内容(值相等)正确比较 Person 对象。 该方法在比较字段之前检查自身比较、null 和类相等性。 Objects.equals 帮助程序处理空安全字段比较。

equals 约定

equals 方法必须遵守 Java 文档中定义的特定约定,以确保可预测的行为

以下测试中使用的 Person.java 类与上一节中定义的相同。

Person.java
package com.zetcode;

import java.util.Objects;

public class Person {

    private String name;
    private int age;
    private String passportNumber;

    public Person(String name, int age, String passportNumber) {
        this.name = name;
        this.age = age;
        this.passportNumber = passportNumber;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
    public String getPassportNumber() { return passportNumber; }

    @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) &&
               Objects.equals(passportNumber, person.passportNumber);
    }

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

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + 
               ", passportNumber='" + passportNumber + "'}";
    }
}

以下 ContractTest 类演示了使用我们的 Person 类的实例的这些约定属性。

ContractTest.java
package com.zetcode;

public class ContractTest {

    public static void main(String[] args) {

        Person a = new Person("Alice", 25, "P123");
        Person b = new Person("Alice", 25, "P123"); // Equal to a
        Person c = new Person("Alice", 25, "P123"); // Equal to a and b
        
        // Reflexive: a.equals(a)
        System.out.println("Reflexive (a.equals(a)): " + a.equals(a)); // true
        
        // Symmetric: a.equals(b) should be same as b.equals(a)
        System.out.println("Symmetric (a.equals(b)): " + a.equals(b));    // true
        System.out.println("Symmetric (b.equals(a)): " + b.equals(a));    // true
        
        // Transitive: if a.equals(b) and b.equals(c), then a.equals(c)
        boolean abEquals = a.equals(b);
        boolean bcEquals = b.equals(c);
        if (abEquals && bcEquals) {
            System.out.println("Transitive (a.equals(c)): " + a.equals(c)); // true
        }
        
        // Consistent: multiple calls to a.equals(b) return same result
        System.out.println("Consistent 1 (a.equals(b)): " + a.equals(b)); // true
        System.out.println("Consistent 2 (a.equals(b)): " + a.equals(b)); // true
        
        // Non-null: a.equals(null) should be false
        System.out.println("Non-null (a.equals(null)): " + a.equals(null)); // false
    }
}

ContractTest 示例有助于验证我们 Person 类的 equals 方法是否满足约定的所有部分。 违反这些规则中的任何一条都可能导致不可预测且不正确的行为,尤其是在 Java 集合框架类中使用对象时。

hashCode 方法

hashCode 方法返回对象的整数哈希码值。此值主要供基于哈希的集合(如 HashMapHashSetHashtable)使用,以高效地存储和检索对象。hashCode 的通用约定至关重要:如果根据 equals(Object) 方法两个对象相等,则对两个对象中的每一个调用 hashCode 方法必须产生相同的整数结果。

如果您重写 equals,则必须也重写 hashCode。我们的 Person 类(为了清楚起见,再次在下面显示)已经包含正确的 hashCode 实现。

Person.java
package com.zetcode;

import java.util.Objects;

public class Person {
    private String name;
    private int age;
    private String passportNumber;

    public Person(String name, int age, String passportNumber) {
        this.name = name;
        this.age = age;
        this.passportNumber = passportNumber;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
    public String getPassportNumber() { return passportNumber; }

    @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) &&
               Objects.equals(passportNumber, person.passportNumber);
    }

    @Override
    public int hashCode() {
        // Use Objects.hash() to combine hash codes of all fields
        // used in the equals() method.
        return Objects.hash(name, age, passportNumber);
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + 
               ", passportNumber='" + passportNumber + "'}";
    }
}

HashCodeDemo 类演示了此 hashCode 的行为。

HashCodeDemo.java
package com.zetcode;

public class HashCodeDemo {

    public static void main(String[] args) {

        Person p1 = new Person("John Doe", 30, "A123456");
        Person p2 = new Person("John Doe", 30, "A123456"); // Equal to p1
        Person p3 = new Person("Jane Smith", 25, "B654321"); // Not equal to p1
        
        System.out.println("p1: " + p1 + ", hashCode: " + p1.hashCode());
        System.out.println("p2: " + p2 + ", hashCode: " + p2.hashCode());
        System.out.println("p3: " + p3 + ", hashCode: " + p3.hashCode());
        
        // Crucial check: if p1.equals(p2) is true, then
        // p1.hashCode() must be equal to p2.hashCode().
        boolean equalsContractMet = p1.equals(p2) && (p1.hashCode() == p2.hashCode());
        System.out.println("Equal objects (p1, p2) have same hash code: " + equalsContractMet); // true

        // Note: If p1.equals(p3) is false, their hash codes are not
        // required to be different, but it's good if they are for performance.
        System.out.println("p1.equals(p3): " + p1.equals(p3)); // false
        System.out.println("Are hashCodes of p1 and p3 different? " +
                           (p1.hashCode() != p3.hashCode())); // usually true
    }
}

此示例显示了 Person 类中使用 Objects.hash 的正确的 hashCode 实现。 此实用程序方法基于所有提供的字段的哈希码计算哈希码。 重要的是,equals 方法中使用的所有字段也用于 hashCode 计算。 相等的对象 (p1p2) 产生相同的哈希码。 不相等的对象 (p1p3) 理想情况下应产生不同的哈希码,以确保哈希表中的良好性能。

hashCode 约定

hashCode 方法必须遵守以下约定

下面测试中使用的 Person.java 类与我们一直在使用的类相同,具有正确的 equalshashCode 方法。

Person.java
package com.zetcode;

import java.util.Objects;

public class Person {

    private String name;
    private int age;
    private String passportNumber;

    public Person(String name, int age, String passportNumber) {
        this.name = name;
        this.age = age;
        this.passportNumber = passportNumber;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
    public String getPassportNumber() { return passportNumber; }

    @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) &&
               Objects.equals(passportNumber, person.passportNumber);
    }

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

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + 
               ", passportNumber='" + passportNumber + "'}";
    }
}

HashCodeContractTest 验证了这些规则。

HashCodeContractTest.java
package com.zetcode;

public class HashCodeContractTest {

    public static void main(String[] args) {

        Person a = new Person("Alice", 25, "P123");
        Person b = new Person("Alice", 25, "P123"); // Equal to a
        Person c = new Person("Bob", 30, "P456");   // Different from a
        
        // Consistency: a.hashCode() called multiple times returns same value
        int hash1 = a.hashCode();
        int hash2 = a.hashCode();
        System.out.println("Consistent hash codes (hash1 == hash2): " + (hash1 == hash2)); // true
        
        // Equality correlation: a.equals(b) implies a.hashCode() == b.hashCode()
        boolean equalsIsTrue = a.equals(b);
        boolean hashCodesEqual = (a.hashCode() == b.hashCode());
        System.out.println("Equal objects, equal hash (a.equals(b) && a.hashCode() == b.hashCode()): " +
                           (equalsIsTrue && hashCodesEqual)); // true
                           
        // Inequality implication: !a.equals(c)
        // a.hashCode() != c.hashCode() is desirable but not guaranteed.
        boolean notEqual = !a.equals(c);
        boolean hashCodesDifferent = (a.hashCode() != c.hashCode());
        System.out.println("Unequal objects (a and c), are their hash codes different? " +
                           hashCodesDifferent); // usually true for good hash functions
        if (notEqual && !hashCodesDifferent) {
            System.out.println("Note: Unequal objects a and c have a hash collision.");
        }
    }
}

HashCodeContractTest 示例使用我们的 Person 类验证 hashCode 约定。 虽然不相等的对象可以具有相同的哈希码(“哈希冲突”),但良好的 hashCode 实现旨在最大限度地减少冲突,以保持高效的哈希表性能。

equals 和 hashCode 的最佳实践

在实现这些方法时,请考虑以下最佳实践

Employee.java
package com.zetcode;

import java.util.Objects;
import java.util.HashMap;
import java.util.Map;

public class Employee {
    private final int id; // Immutable, unique identifier
    private String name;
    private String department;
    
    public Employee(int id, String name, String department) {
        if (name == null || department == null) {
            throw new NullPointerException("Name and department cannot be null");
        }
        this.id = id; // 'id' is final, set only once
        this.name = name;
        this.department = department;
    }
    
    public int getId() { return id; }
    public String getName() { return name; }
    public void setName(String name) {
        if (name == null) throw new NullPointerException("Name cannot be null");
        this.name = name;
    }
    public String getDepartment() { return department; }
    public void setDepartment(String department) {
        if (department == null) throw new NullPointerException("Department cannot be null");
        this.department = department;
    }

    @Override
    public final boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Employee)) return false; 
        Employee employee = (Employee) o;
        return id == employee.id;
    }
    
    @Override
    public final int hashCode() {
        return Objects.hash(id);
    }
    
    @Override
    public String toString() {
        return "Employee{id=" + id + ", name='" + name + "', department='" + department + "'}";
    }

    public static void main(String[] args) {

        Employee e1 = new Employee(101, "John Smith", "Engineering");
        Employee e2 = new Employee(101, "Johnathan Smith", "HR"); 
        Employee e3 = new Employee(102, "Alice Wonderland", "Engineering");
        
        System.out.println("e1 equals e2 (same ID): " + e1.equals(e2)); // true
        System.out.println("e1 hashCode == e2 hashCode: " + (e1.hashCode() == e2.hashCode())); // true

        Map<Employee, String> employeeMap = new HashMap<>();
        employeeMap.put(e1, "Access Card A");
        e1.setDepartment("Management"); // Change mutable field
        // Still retrievable as equals/hashCode depend only on 'id'
        System.out.println("Value for e1 after department change: " + employeeMap.get(e1)); // Access Card A
        System.out.println("Value for e2 (equal to e1): " + employeeMap.get(e2)); // Access Card A
    }
}

Employee 示例演示了一个类,其中相等性仅基于不可变的唯一标识符 (id)。可变字段(namedepartment)不参与 equalshashCode 计算。这确保了 Employee 对象的哈希码保持恒定,并且其相等性稳定,即使可变属性发生更改,也可以安全地用于基于哈希的集合。

常见陷阱

在实现 equalshashCode 时,可能会发生几种常见的错误,从而导致细微的错误

PitfallsDemo.java
package com.zetcode;

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

public class PitfallsDemo {

    static class BadSymmetryPoint {
        private final int x;
        private final int y;
        public BadSymmetryPoint(int x, int y) { this.x = x; this.y = y; }
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o instanceof BadSymmetryPoint) {
                BadSymmetryPoint that = (BadSymmetryPoint) o;
                return x == that.x && y == that.y;
            }
            if (o instanceof String) { // Problematic comparison
                return o.equals(String.format("(%d,%d)", x, y));
            }
            return false;
        }
        @Override
        public int hashCode() { return Objects.hash(x,y); }
    }
    
    static class MutableKey {
        private int value;
        public MutableKey(int value) { this.value = value; }
        public void setValue(int value) { this.value = value; }
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            return value == ((MutableKey) o).value;
        }
        @Override
        public int hashCode() { return Objects.hash(value); }
        @Override
        public String toString() { return "MutableKey{value=" + value + "}"; }
    }

    public static void main(String[] args) {

        BadSymmetryPoint p1 = new BadSymmetryPoint(1, 2);
        String s1 = "(1,2)";
        System.out.println("p1.equals(s1): " + p1.equals(s1)); // true
        System.out.println("s1.equals(p1): " + s1.equals(p1)); // false (Symmetry violated!)
        System.out.println("---");

        Set<MutableKey> keySet = new HashSet<>();
        MutableKey key = new MutableKey(42);
        keySet.add(key);
        System.out.println("Set contains key (value 42): " + keySet.contains(key)); // true
        key.setValue(99); // Modify key's state (and hashCode)
        System.out.println("Set contains modified key (value 99): " + keySet.contains(key)); // false!
    }
}

PitfallsDemo 类说明了两个常见问题。 首先,BadSymmetryPoint 违反了 equals 中的对称性。 其次,MutableKey 显示了为什么在哈希集合中使用可变对象作为键是危险的:在插入后修改键可能会使其“丢失”。

Lombok 和 IDE 生成

手动编写 equalshashCode 容易出错。 诸如 Project Lombok(带有 @EqualsAndHashCode)和 IDE 代码生成功能之类的工具可以自动创建这些方法,从而减少样板代码和潜在错误,同时确保遵守约定。

ToolAssistedExample.java
package com.zetcode;

import lombok.EqualsAndHashCode; 
import java.util.Objects;

@EqualsAndHashCode // Lombok: generates equals() and hashCode()
class LombokPerson {
    private String name;
    private int age;
    private String email;
    public LombokPerson(String name, int age, String email) {
        this.name = name; this.age = age; this.email = email;
    }
}

class IDEGeneratedPerson { // Example of IDE-generated methods
    private String name;
    private int age;
    private String email;
    public IDEGeneratedPerson(String n, int a, String e) { name=n; age=a; email=e; }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        IDEGeneratedPerson p = (IDEGeneratedPerson) o;
        return age == p.age && Objects.equals(name, p.name) && Objects.equals(email, p.email);
    }
    @Override
    public int hashCode() { return Objects.hash(name, age, email); }
}

public class ToolAssistedExample {
    public static void main(String[] args) {

        LombokPerson lp1 = new LombokPerson("Alice", 30, "a@e.com");
        LombokPerson lp2 = new LombokPerson("Alice", 30, "a@e.com");
        System.out.println("Lombok: lp1 equals lp2: " + lp1.equals(lp2)); // true
        
        IDEGeneratedPerson ip1 = new IDEGeneratedPerson("Carol", 35, "c@e.com");
        IDEGeneratedPerson ip2 = new IDEGeneratedPerson("Carol", 35, "c@e.com");
        System.out.println("IDE: ip1 equals ip2: " + ip1.equals(ip2)); // true
    }
}

此示例展示了 Lombok 和典型的 IDE 生成。 这些工具可帮助保持正确性,但理解基本原理仍然至关重要。

使用 Java Records 的简化实现

自 Java 14(在 Java 16 中标准化)以来,记录提供了一种简洁的方式来创建不可变的数据载体。编译器会自动为所有记录组件生成 equalshashCodetoString、规范构造函数和访问器方法。

RecordExample.java
package com.zetcode;

import java.util.HashSet;
import java.util.Set;

record UserRecord(int id, String username, String email) {
    // Compiler auto-generates constructor, accessors,
    // equals(), hashCode(), and toString() based on all components.
}

public class RecordExample {
    public static void main(String[] args) {

        UserRecord user1 = new UserRecord(1, "john.doe", "john.doe@example.com");
        UserRecord user2 = new UserRecord(1, "john.doe", "john.doe@example.com");
        UserRecord user3 = new UserRecord(2, "jane.doe", "jane.doe@example.com");

        System.out.println("user1 equals user2: " + user1.equals(user2)); // true
        System.out.println("user1 hashCode == user2 hashCode: " + 
                           (user1.hashCode() == user2.hashCode())); // true

        Set<UserRecord> userSet = new HashSet<>();
        userSet.add(user1);
        System.out.println("Set contains user2 (equal to user1): " + 
                           userSet.contains(user2)); // true
        System.out.println("User1 details: " + user1.toString());
    }
}

作为 Java 记录的 UserRecord 类会自动获得正确的 equalshashCode。 这大大减少了数据类的样板代码,并确保遵守约定。

来源

Java 语言规范:Object.equals()
Java 语言规范:Object.hashCode()
Java 语言特性:记录

在本文中,我们研究了 Java 的 equalshashCode。 我们涵盖了约定、实现、最佳实践、陷阱和现代方法。 正确的理解对于正确的对象比较和 Java 集合中的可靠行为至关重要。

作者

我叫 Jan Bodnar,是一名敬业的程序员,在该领域拥有多年的经验。 我于 2007 年开始撰写编程文章,此后撰写了 1,400 多篇文章和八本电子书。 凭借八年以上的教学经验,我致力于分享我的知识并帮助他人掌握编程概念。

列出所有Java教程