equals和hashCode方法的关系

问题描述

如果重写了equals方法,为什么还要重写hashCode方法?如果没有重写hashCode方法,会有什么问题?

详细解答

核心原则

equals和hashCode的契约(Contract)

  1. 如果两个对象通过equals()比较相等,那么它们的hashCode()必须返回相同的值
  2. 如果两个对象的hashCode()相同,它们不一定通过equals()相等(但最好尽量避免)
  3. 如果重写了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

  1. 维护契约:确保相等对象有相同hashCode
  2. 正确的集合行为:HashMap、HashSet等依赖此契约
  3. 性能保证:避免哈希冲突导致的性能下降
  4. 程序正确性:防止逻辑错误和查找失败

最佳实践

  • 使用Objects.hash()简化实现
  • 确保equals和hashCode使用相同字段
  • 考虑性能和哈希分布的均匀性
  • 遵循不可变对象原则(如果可能)
powered by Gitbook© 2025 编外计划 | 最后修改: 2025-07-28 18:05:38

results matching ""

    No results matching ""