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教程。