1. 概述

哈希是计算机科学的基础概念。在Java中,高效的哈希算法支撑着许多核心集合类,比如 HashMapHashSet

本文将深入探讨 hashCode() 的工作原理、在集合中的应用,以及如何正确实现它。

2. 在数据结构中使用 hashCode()

某些场景下,集合的基本操作可能效率低下。例如,以下代码会触发线性搜索,对于大型列表极其低效:

List<String> words = Arrays.asList("Welcome", "to", "Baeldung");
if (words.contains("Baeldung")) {
    System.out.println("Baeldung is in the list");
}

Java提供了专门解决此类问题的数据结构,如多个 Map 实现就是哈希表。使用哈希表时,集合会通过 hashCode() 方法计算键的哈希值,并用该值内部存储数据,使访问操作效率大幅提升。

3. 理解 hashCode() 的工作机制

简单说,hashCode() 返回一个由哈希算法生成的整数值。

✅ 相等的对象(根据 equals() 判断)必须返回相同的哈希值
❌ 不同的对象无需返回不同的哈希值

hashCode() 的通用契约要求:

  1. 一致性:在单次应用执行中,只要对象参与 equals() 比较的信息未被修改,多次调用 hashCode() 必须返回相同值(不同执行间无需保持一致)
  2. 对等性:若 equals() 判定两对象相等,则调用 hashCode() 必须返回相同值
  3. 差异性:若 equals() 判定两对象不等,调用 hashCode() 无需返回不同值,但为提高哈希表性能,建议生成不同结果

"Object 类的 hashCode() 实现会尽可能为不同对象返回不同整数(通常通过转换对象内存地址实现,但这并非语言要求)"

4. 简单粗暴的 hashCode() 实现

一个完全符合契约的 hashCode() 实现可以非常简单。以下 User 类展示了这种实现:

public class User {

    private long id;
    private String name;
    private String email;

    // 标准getter/setter/构造方法
        
    @Override
    public int hashCode() {
        return 1;
    }
        
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null) return false;
        if (this.getClass() != o.getClass()) return false;
        User user = (User) o;
        return id == user.id 
          && (name.equals(user.name) 
          && email.equals(user.email));
    }
    
    // getter和setter方法
}

虽然这种实现合法,但它会使哈希表退化——所有对象存储在同一个桶中,导致查找操作变为线性扫描,完全失去哈希表优势。我们将在第7节深入探讨。

5. 改进 hashCode() 实现

通过结合所有字段,我们可以改进实现,为不同对象生成不同哈希值:

@Override
public int hashCode() {
    return (int) id * name.hashCode() * email.hashCode();
}

这个基础算法明显优于前一个版本——它将 idnameemail 的哈希码相乘生成对象哈希值。只要 equals() 实现保持一致,这就是合理的 hashCode() 实现。

6. 标准 hashCode() 实现

哈希算法越优秀,哈希表性能越好。下面展示使用质数(31)增强唯一性的"标准"实现:

@Override
public int hashCode() {
    int hash = 7;
    hash = 31 * hash + (int) id;
    hash = 31 * hash + (name == null ? 0 : name.hashCode());
    hash = 31 * hash + (email == null ? 0 : email.hashCode());
    return hash;
}

⚠️ 虽然需要理解原理,但无需每次手动实现。现代IDE都能自动生成 hashCode()equals()。Java 7+ 还提供了便捷工具方法:

Objects.hash(name, email)

IDE生成示例
IntelliJ IDEA 生成:

@Override
public int hashCode() {
    int result = (int) (id ^ (id >>> 32));
    result = 31 * result + name.hashCode();
    result = 31 * result + email.hashCode();
    return result;
}

Eclipse 生成:

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((email == null) ? 0 : email.hashCode());
    result = prime * result + (int) (id ^ (id >>> 32));
    result = prime * result + ((name == null) ? 0 : name.hashCode());
    return result;
}

自动化工具
使用 Lombok 时,只需添加依赖:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.30</version>
</dependency>

然后注解类:

@EqualsAndHashCode 
public class User {
    // 字段和方法
}

使用 Apache Commons Lang:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.14.0</version>
</dependency>

实现方式:

public int hashCode() {
    return new HashCodeBuilder(17, 37)
        .append(id)
        .append(name)
        .append(email)
        .toHashCode();
}

为什么常用31?
31有个特殊性质:31 * i == (i << 5) - i,位运算比标准乘法更快。

7. 处理哈希冲突

即使算法优秀,不同对象仍可能产生相同哈希值——这就是哈希冲突。Java 的 HashMap 使用链地址法处理冲突:

"当多个对象指向同一桶时,它们以链表形式存储。此时哈希表变为链表数组,相同哈希值的对象追加到对应桶的链表末尾。最坏情况下,多个桶形成长链表,对象检索退化为线性操作。"

Java 8 对此做了优化:当桶大小超过阈值时,链表会转为红黑树,将查找复杂度从 O(n) 优化到 O(log n)。

8. 创建测试应用

下面通过示例验证 hashCode() 的实际行为。创建一个应用,将 User 对象存入 HashMap,并在调用时打印日志:

public class Application {

    public static void main(String[] args) {
        Map<User, User> users = new HashMap<>();
        User user1 = new User(1L, "John", "john.doe@example.com");
        User user2 = new User(2L, "Jennifer", "jennifer.smith@example.com");
        User user3 = new User(3L, "Mary", "mary.jones@example.com");

        users.put(user1, user1);
        users.put(user2, user2);
        users.put(user3, user3);
        if (users.containsKey(user1)) {
            System.out.print("User found in the collection");
        }
    }
}

User 类的 hashCode() 实现:

public class User {
    // ... 其他代码

    public int hashCode() {
        int hash = 7;
        hash = 31 * hash + (int) id;
        hash = 31 * hash + (name == null ? 0 : name.hashCode());
        hash = 31 * hash + (email == null ? 0 : email.hashCode());
        logger.info("hashCode() called - Computed hash: " + hash);
        return hash;
    }
}

每次对象存入哈希表或调用 containsKey() 时,hashCode() 都会被调用,输出如下:

[main] INFO com.baeldung.entities.User - hashCode() called - Computed hash: 1255477819
[main] INFO com.baeldung.entities.User - hashCode() called - Computed hash: -282948472
[main] INFO com.baeldung.entities.User - hashCode() called - Computed hash: -1540702691
[main] INFO com.baeldung.entities.User - hashCode() called - Computed hash: 1255477819
User found in the collection

9. 总结

高效实现 hashCode() 需要结合数学概念(质数/任意数)、逻辑和基础运算。但即使不深究这些技巧,只要确保:

  1. 不等对象生成不同哈希值
  2. equals() 实现保持一致

就能有效实现 hashCode()。推荐阅读《Effective Java》获取更全面的实现指南。

所有示例代码可在 GitHub 获取。


原始标题:Guide to hashCode() in Java