1. 概述
哈希是计算机科学的基础概念。在Java中,高效的哈希算法支撑着许多核心集合类,比如 HashMap
和 HashSet
。
本文将深入探讨 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()
的通用契约要求:
- 一致性:在单次应用执行中,只要对象参与
equals()
比较的信息未被修改,多次调用hashCode()
必须返回相同值(不同执行间无需保持一致) - 对等性:若
equals()
判定两对象相等,则调用hashCode()
必须返回相同值 - 差异性:若
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();
}
这个基础算法明显优于前一个版本——它将 id
和 name
、email
的哈希码相乘生成对象哈希值。只要 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()
需要结合数学概念(质数/任意数)、逻辑和基础运算。但即使不深究这些技巧,只要确保:
- 不等对象生成不同哈希值
- 与
equals()
实现保持一致
就能有效实现 hashCode()
。推荐阅读《Effective Java》获取更全面的实现指南。
所有示例代码可在 GitHub 获取。