equals和hashCode方法的关系
问题描述
如果重写了equals方法,为什么还要重写hashCode方法?如果没有重写hashCode方法,会有什么问题?
详细解答
核心原则
equals和hashCode的契约(Contract):
- 如果两个对象通过
equals()
比较相等,那么它们的hashCode()
必须返回相同的值 - 如果两个对象的
hashCode()
相同,它们不一定通过equals()
相等(但最好尽量避免) - 如果重写了
equals()
,就必须重写hashCode()
来维持这个契约
基础验证
1. 违反契约的问题演示
import java.util.*;
// 错误示例:只重写equals,不重写hashCode
class BadPerson {
private String name;
private int age;
public BadPerson(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
BadPerson person = (BadPerson) obj;
return age == person.age && Objects.equals(name, person.name);
}
// 没有重写hashCode!!!
public String getName() { return name; }
public int getAge() { return age; }
@Override
public String toString() {
return "BadPerson{name='" + name + "', age=" + age + "}";
}
}
public class HashCodeProblemDemo {
public static void main(String[] args) {
BadPerson p1 = new BadPerson("张三", 25);
BadPerson p2 = new BadPerson("张三", 25);
System.out.println("=== equals vs hashCode 不一致的问题 ===");
System.out.println("p1.equals(p2): " + p1.equals(p2)); // true
System.out.println("p1.hashCode(): " + p1.hashCode()); // 不同的值
System.out.println("p2.hashCode(): " + p2.hashCode()); // 不同的值
System.out.println("hashCode相等: " + (p1.hashCode() == p2.hashCode())); // false
// 在HashSet中的问题
demonstrateHashSetProblem();
// 在HashMap中的问题
demonstrateHashMapProblem();
}
public static void demonstrateHashSetProblem() {
System.out.println("\n=== HashSet中的问题 ===");
Set<BadPerson> set = new HashSet<>();
BadPerson p1 = new BadPerson("李四", 30);
BadPerson p2 = new BadPerson("李四", 30);
set.add(p1);
set.add(p2); // 虽然equals相等,但会被添加进去
System.out.println("Set大小: " + set.size()); // 2,应该是1
System.out.println("contains p1: " + set.contains(p1)); // true
System.out.println("contains p2: " + set.contains(p2)); // true,但p1==p2应该只有一个
// 更严重的问题:查找失败
BadPerson p3 = new BadPerson("李四", 30);
System.out.println("contains p3: " + set.contains(p3)); // false!虽然逻辑上相等
}
public static void demonstrateHashMapProblem() {
System.out.println("\n=== HashMap中的问题 ===");
Map<BadPerson, String> map = new HashMap<>();
BadPerson key1 = new BadPerson("王五", 35);
map.put(key1, "员工信息1");
// 用相等的对象作为key查找
BadPerson key2 = new BadPerson("王五", 35);
String value = map.get(key2);
System.out.println("key1.equals(key2): " + key1.equals(key2)); // true
System.out.println("用key2查找到的值: " + value); // null!
System.out.println("map大小: " + map.size()); // 1
// 显示内部结构问题
System.out.println("key1的hash位置: " + (key1.hashCode() & 15));
System.out.println("key2的hash位置: " + (key2.hashCode() & 15));
}
}
2. 正确的实现
// 正确示例:同时重写equals和hashCode
class GoodPerson {
private String name;
private int age;
public GoodPerson(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
GoodPerson person = (GoodPerson) obj;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age); // 使用相同的字段
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public String toString() {
return "GoodPerson{name='" + name + "', age=" + age + "}";
}
}
public class CorrectImplementationDemo {
public static void main(String[] args) {
GoodPerson p1 = new GoodPerson("张三", 25);
GoodPerson p2 = new GoodPerson("张三", 25);
System.out.println("=== 正确的实现 ===");
System.out.println("p1.equals(p2): " + p1.equals(p2)); // true
System.out.println("p1.hashCode(): " + p1.hashCode()); // 相同的值
System.out.println("p2.hashCode(): " + p2.hashCode()); // 相同的值
System.out.println("hashCode相等: " + (p1.hashCode() == p2.hashCode())); // true
testHashSet();
testHashMap();
}
public static void testHashSet() {
System.out.println("\n=== HashSet正常工作 ===");
Set<GoodPerson> set = new HashSet<>();
GoodPerson p1 = new GoodPerson("李四", 30);
GoodPerson p2 = new GoodPerson("李四", 30);
set.add(p1);
set.add(p2); // 不会重复添加
System.out.println("Set大小: " + set.size()); // 1
System.out.println("contains p1: " + set.contains(p1)); // true
System.out.println("contains p2: " + set.contains(p2)); // true
GoodPerson p3 = new GoodPerson("李四", 30);
System.out.println("contains p3: " + set.contains(p3)); // true,正确找到
}
public static void testHashMap() {
System.out.println("\n=== HashMap正常工作 ===");
Map<GoodPerson, String> map = new HashMap<>();
GoodPerson key1 = new GoodPerson("王五", 35);
map.put(key1, "员工信息1");
GoodPerson key2 = new GoodPerson("王五", 35);
String value = map.get(key2);
System.out.println("key1.equals(key2): " + key1.equals(key2)); // true
System.out.println("用key2查找到的值: " + value); // "员工信息1"
System.out.println("key1的hash位置: " + (key1.hashCode() & 15));
System.out.println("key2的hash位置: " + (key2.hashCode() & 15)); // 相同位置
}
}
hashCode实现的最佳实践
1. 使用Objects.hash()工具方法
public class HashCodeBestPractices {
// 方法1:使用Objects.hash()(推荐)
static class Employee {
private String name;
private int id;
private String department;
public Employee(String name, int id, String department) {
this.name = name;
this.id = id;
this.department = department;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Employee employee = (Employee) obj;
return id == employee.id &&
Objects.equals(name, employee.name) &&
Objects.equals(department, employee.department);
}
@Override
public int hashCode() {
return Objects.hash(name, id, department);
}
@Override
public String toString() {
return String.format("Employee{name='%s', id=%d, dept='%s'}", name, id, department);
}
}
// 方法2:手动实现(了解原理)
static class Product {
private String name;
private double price;
private String category;
public Product(String name, double price, String category) {
this.name = name;
this.price = price;
this.category = category;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Product product = (Product) obj;
return Double.compare(product.price, price) == 0 &&
Objects.equals(name, product.name) &&
Objects.equals(category, product.category);
}
@Override
public int hashCode() {
int result = 17; // 非零初始值
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + Double.hashCode(price);
result = 31 * result + (category != null ? category.hashCode() : 0);
return result;
}
@Override
public String toString() {
return String.format("Product{name='%s', price=%.2f, category='%s'}", name, price, category);
}
}
public static void main(String[] args) {
testEmployeeHashCode();
testProductHashCode();
testHashDistribution();
}
public static void testEmployeeHashCode() {
System.out.println("=== Employee测试 ===");
Employee e1 = new Employee("张三", 1001, "开发部");
Employee e2 = new Employee("张三", 1001, "开发部");
Employee e3 = new Employee("李四", 1002, "测试部");
System.out.println("e1.hashCode(): " + e1.hashCode());
System.out.println("e2.hashCode(): " + e2.hashCode());
System.out.println("e3.hashCode(): " + e3.hashCode());
System.out.println("e1.equals(e2): " + e1.equals(e2));
System.out.println("e1.hashCode() == e2.hashCode(): " + (e1.hashCode() == e2.hashCode()));
}
public static void testProductHashCode() {
System.out.println("\n=== Product测试 ===");
Product p1 = new Product("笔记本", 5999.99, "电子产品");
Product p2 = new Product("笔记本", 5999.99, "电子产品");
Product p3 = new Product("鼠标", 199.99, "电子产品");
System.out.println("p1.hashCode(): " + p1.hashCode());
System.out.println("p2.hashCode(): " + p2.hashCode());
System.out.println("p3.hashCode(): " + p3.hashCode());
System.out.println("p1.equals(p2): " + p1.equals(p2));
System.out.println("p1.hashCode() == p2.hashCode(): " + (p1.hashCode() == p2.hashCode()));
}
public static void testHashDistribution() {
System.out.println("\n=== 哈希分布测试 ===");
Map<Integer, Integer> distribution = new HashMap<>();
int buckets = 16; // 模拟HashMap的桶数
// 测试1000个Employee对象的哈希分布
for (int i = 0; i < 1000; i++) {
Employee emp = new Employee("Employee" + i, 1000 + i, "部门" + (i % 10));
int bucket = Math.abs(emp.hashCode()) % buckets;
distribution.merge(bucket, 1, Integer::sum);
}
System.out.println("哈希桶分布(理想情况下应该比较均匀):");
for (int i = 0; i < buckets; i++) {
int count = distribution.getOrDefault(i, 0);
System.out.printf("桶%2d: %3d个对象 %s%n", i, count, "█".repeat(count / 10));
}
}
}
性能影响分析
public class PerformanceImpact {
// 性能测试用的简单类
static class SimpleKey {
private final int id;
private final String name;
public SimpleKey(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
SimpleKey simpleKey = (SimpleKey) obj;
return id == simpleKey.id && Objects.equals(name, simpleKey.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
}
public static void main(String[] args) {
testHashMapPerformance();
testHashSetPerformance();
compareWithLinkedList();
}
public static void testHashMapPerformance() {
System.out.println("=== HashMap性能测试 ===");
Map<SimpleKey, String> map = new HashMap<>();
int size = 100_000;
// 插入性能测试
long start = System.nanoTime();
for (int i = 0; i < size; i++) {
SimpleKey key = new SimpleKey(i, "Name" + i);
map.put(key, "Value" + i);
}
long insertTime = System.nanoTime() - start;
// 查找性能测试
start = System.nanoTime();
for (int i = 0; i < size; i++) {
SimpleKey key = new SimpleKey(i, "Name" + i);
String value = map.get(key);
}
long lookupTime = System.nanoTime() - start;
System.out.printf("插入%d个元素耗时: %.2f ms%n", size, insertTime / 1_000_000.0);
System.out.printf("查找%d个元素耗时: %.2f ms%n", size, lookupTime / 1_000_000.0);
System.out.println("平均查找时间: O(1) - 常数时间");
}
public static void testHashSetPerformance() {
System.out.println("\n=== HashSet性能测试 ===");
Set<SimpleKey> set = new HashSet<>();
int size = 100_000;
// 插入测试
long start = System.nanoTime();
for (int i = 0; i < size; i++) {
SimpleKey key = new SimpleKey(i, "Name" + i);
set.add(key);
}
long insertTime = System.nanoTime() - start;
// 查找测试
start = System.nanoTime();
for (int i = 0; i < size; i++) {
SimpleKey key = new SimpleKey(i, "Name" + i);
boolean contains = set.contains(key);
}
long lookupTime = System.nanoTime() - start;
System.out.printf("插入%d个元素耗时: %.2f ms%n", size, insertTime / 1_000_000.0);
System.out.printf("查找%d个元素耗时: %.2f ms%n", size, lookupTime / 1_000_000.0);
}
public static void compareWithLinkedList() {
System.out.println("\n=== 与LinkedList性能对比 ===");
int size = 10_000; // 较小的size,因为LinkedList查找是O(n)
// LinkedList测试
List<SimpleKey> list = new LinkedList<>();
for (int i = 0; i < size; i++) {
list.add(new SimpleKey(i, "Name" + i));
}
long start = System.nanoTime();
for (int i = 0; i < size; i++) {
SimpleKey target = new SimpleKey(i, "Name" + i);
boolean found = list.contains(target); // O(n)查找
}
long listTime = System.nanoTime() - start;
// HashSet测试
Set<SimpleKey> set = new HashSet<>(list);
start = System.nanoTime();
for (int i = 0; i < size; i++) {
SimpleKey target = new SimpleKey(i, "Name" + i);
boolean found = set.contains(target); // O(1)查找
}
long setTime = System.nanoTime() - start;
System.out.printf("LinkedList查找%d个元素耗时: %.2f ms (O(n))%n", size, listTime / 1_000_000.0);
System.out.printf("HashSet查找%d个元素耗时: %.2f ms (O(1))%n", size, setTime / 1_000_000.0);
System.out.printf("HashSet比LinkedList快 %.1f 倍%n", (double) listTime / setTime);
}
}
总结
为什么重写equals必须重写hashCode:
- 维护契约:确保相等对象有相同hashCode
- 正确的集合行为:HashMap、HashSet等依赖此契约
- 性能保证:避免哈希冲突导致的性能下降
- 程序正确性:防止逻辑错误和查找失败
最佳实践:
- 使用
Objects.hash()
简化实现 - 确保equals和hashCode使用相同字段
- 考虑性能和哈希分布的均匀性
- 遵循不可变对象原则(如果可能)