Java equals 和 hashCode
上次修改:2025 年 5 月 7 日
equals 和 hashCode 方法在 Java 的对象比较和哈希机制中起着至关重要的作用。 一个良好实现的 equals 方法确保了准确的对象相等性检查,而一个优化的 hashCode 方法使得在基于哈希的集合(如 HashMap 和 HashSet)中高效地使用对象成为可能。 正确重写这些方法对于在 Java 应用程序中维护一致且可预测的行为至关重要。
这些方法最初定义在 Object 类中,它是 Java 类层次结构的根。 由于所有 Java 对象默认继承这些方法,因此理解它们的行为至关重要。 默认情况下,equals 检查引用相等性(即两个对象引用是否指向同一内存地址),而 hashCode 根据对象的内存位置生成唯一标识符。
然而,对于自定义类,通常需要重写这些方法,以便根据对象的状态(例如属性值)而不是引用标识提供有意义的相等性比较。 重写 equals 时,请确保与 hashCode 一致——根据 equals 认为相等的对象必须返回相同的哈希码。 这种一致性可防止基于哈希的集合中出现意外行为,并确保高效的数据检索。
equals 方法
equals 方法用于比较两个对象是否相等。Object 类中的默认实现提供引用相等性(使用 == 运算符)。大多数类应该重写此方法以提供基于值的相等性,比较对象的实际内容。
让我们定义一个 Person 类,我们将在示例中使用它。 它包括姓名、年龄和护照号码等字段,以及正确实现的 equals 和 hashCode 方法。
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 方法的用法。
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 文档中定义的特定约定,以确保可预测的行为
- 自反性:对于任何非空引用值 x,
x.equals(x)必须返回true。 - 对称性:对于任何非空引用值 x 和 y,当且仅当
y.equals(x)返回true时,x.equals(y)必须返回true。 - 传递性:对于任何非空引用值 x、y 和 z,如果
x.equals(y)返回true并且y.equals(z)返回true,则x.equals(z)必须返回true。 - 一致性:对于任何非空引用值 x 和 y,如果未修改对象上
equals比较中使用的任何信息,则多次调用x.equals(y)始终返回true或始终返回false。 - 非空性:对于任何非空引用值 x,
x.equals(null)必须返回false。
以下测试中使用的 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 类的实例的这些约定属性。
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 方法返回对象的整数哈希码值。此值主要供基于哈希的集合(如 HashMap、HashSet 和 Hashtable)使用,以高效地存储和检索对象。hashCode 的通用约定至关重要:如果根据 equals(Object) 方法两个对象相等,则对两个对象中的每一个调用 hashCode 方法必须产生相同的整数结果。
如果您重写 equals,则必须也重写 hashCode。我们的 Person 类(为了清楚起见,再次在下面显示)已经包含正确的 hashCode 实现。
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 的行为。
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 计算。 相等的对象 (p1 和 p2) 产生相同的哈希码。 不相等的对象 (p1 和 p3) 理想情况下应产生不同的哈希码,以确保哈希表中的良好性能。
hashCode 约定
hashCode 方法必须遵守以下约定
- 一致性:在应用程序的同一次执行期间,如果
equals比较中使用的对象的字段没有更改,则多次调用hashCode必须始终返回相同的整数。此整数不需要在应用程序的一次执行与另一次执行之间保持一致。 - 相等性相关性:如果根据
equals(Object)方法两个对象相等,则对两个对象中的每一个调用hashCode必须产生相同的整数结果。 - 不等性暗示(理想情况,非严格要求):如果根据
equals(Object)方法两个对象不相等,则不要求对两个对象中的每一个调用hashCode产生不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同的整数结果可能会提高哈希表的性能。
下面测试中使用的 Person.java 类与我们一直在使用的类相同,具有正确的 equals 和 hashCode 方法。
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 验证了这些规则。
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 的最佳实践
在实现这些方法时,请考虑以下最佳实践
- 同时重写或都不重写:如果您重写
equals,则必须重写hashCode,反之亦然,以维护约定。 - 使用相同的字段:用于计算
equals的字段集应与用于计算hashCode的字段集相同。 - 保持一致性:确保您的实现遵守这两种方法的约定。
- 利用帮助程序类:使用
java.util.Objects.equals进行空安全字段比较,使用java.util.Objects.hash从多个字段方便地生成哈希码。 - 性能:保持
equals和hashCode方法快速且确定。避免复杂的计算或 I/O 操作。 - 可变性:小心使用可变对象。如果对象在用作哈希映射中的键或哈希集中的元素后状态发生更改,则集合的行为可能会不可预测。如果对象是可变的,请清楚地记录,以及这如何影响相等性和哈希。如果可能,请考虑使参与哈希的对象不可变。
- Final 方法:如果您想阻止子类更改其行为,请考虑将
equals和hashCode设置为final,如果子类的实例与集合中的超类实例混合在一起,这一点可能很重要。
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)。可变字段(name 和 department)不参与 equals 或 hashCode 计算。这确保了 Employee 对象的哈希码保持恒定,并且其相等性稳定,即使可变属性发生更改,也可以安全地用于基于哈希的集合。
常见陷阱
在实现 equals 和 hashCode 时,可能会发生几种常见的错误,从而导致细微的错误
- 重写
equals时不重写hashCode:这违反了约定,并导致基于哈希的集合出现问题。 - 违反
equals约定:尤其是对称性或传递性,通常在处理继承或混合类型时。 - 在集合中使用可变字段作为
hashCode的键:如果在将对象添加到哈希集或作为哈希映射中的键之后,hashCode中使用的字段发生更改,则对象可能会“丢失”。 - 不正确的
equals实现:例如,以破坏对称性的方式使用instanceof,或者不处理null。使用getClass() != o.getClass对于健壮的相等性通常更安全。
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 生成
手动编写 equals 和 hashCode 容易出错。 诸如 Project Lombok(带有 @EqualsAndHashCode)和 IDE 代码生成功能之类的工具可以自动创建这些方法,从而减少样板代码和潜在错误,同时确保遵守约定。
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 中标准化)以来,记录提供了一种简洁的方式来创建不可变的数据载体。编译器会自动为所有记录组件生成 equals、hashCode、toString、规范构造函数和访问器方法。
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 类会自动获得正确的 equals 和 hashCode。 这大大减少了数据类的样板代码,并确保遵守约定。
来源
Java 语言规范:Object.equals()
Java 语言规范:Object.hashCode()
Java 语言特性:记录
在本文中,我们研究了 Java 的 equals 和 hashCode。 我们涵盖了约定、实现、最佳实践、陷阱和现代方法。 正确的理解对于正确的对象比较和 Java 集合中的可靠行为至关重要。
作者
列出所有Java教程。