问题
两个对象的 hashCode 相同,equals 是否一定为 true?
答案
核心结论
不一定为 true。两个对象的 hashCode 相同,只能说明它们可能相等,但不能保证一定相等。这种现象称为哈希冲突(Hash Collision)。
hashCode 与 equals 的约定
Java 规范要求 hashCode 和 equals 必须遵守以下契约:
| 规则 | 说明 | 示例 |
|---|---|---|
| 规则1 | 如果 a.equals(b) 为 true,则 a.hashCode() == b.hashCode() 必须为 true | 相等对象必须有相同哈希码 |
| 规则2 | 如果 a.hashCode() == b.hashCode(),a.equals(b) 不一定为 true | 哈希码相同不代表对象相等 |
| 规则3 | 如果 a.equals(b) 为 false,a.hashCode() 和 b.hashCode() 可以相同或不同 | 不相等对象可以有相同哈希码 |
原理分析
1. 哈希冲突的本质
public class HashCodeDemo {
public static void main(String[] args) {
// 示例1:不同字符串可能有相同 hashCode
String s1 = "Aa";
String s2 = "BB";
System.out.println(s1.hashCode()); // 2112
System.out.println(s2.hashCode()); // 2112
System.out.println(s1.equals(s2)); // false
// 示例2:自定义对象的哈希冲突
Person p1 = new Person("张三", 25);
Person p2 = new Person("李四", 25);
System.out.println(p1.hashCode() == p2.hashCode()); // 可能为 true
System.out.println(p1.equals(p2)); // false
}
}
2. 为什么会发生哈希冲突?
// String 的 hashCode 实现(简化版)
public int hashCode() {
int h = 0;
for (int i = 0; i < length; i++) {
h = 31 * h + charAt(i);
}
return h;
}
// 计算 "Aa" 的 hashCode
// 'A' = 65, 'a' = 97
// h = 31 * 0 + 65 = 65
// h = 31 * 65 + 97 = 2112
// 计算 "BB" 的 hashCode
// 'B' = 66
// h = 31 * 0 + 66 = 66
// h = 31 * 66 + 66 = 2112
// 结果:不同字符串产生相同哈希码
根本原因:
- hashCode 返回 int 类型(32位,约 42 亿个值)
- 对象数量理论上无限
- 根据鸽巢原理,必然存在不同对象映射到相同哈希码
HashMap 中的应用
1. HashMap 的存储原理
public class HashMap<K,V> {
// 简化的 put 方法逻辑
public V put(K key, V value) {
int hash = hash(key); // 1. 计算 hashCode
int index = hash & (n - 1); // 2. 定位数组下标
Node<K,V> node = table[index]; // 3. 获取链表/红黑树头节点
// 4. 遍历链表,使用 equals 判断是否存在相同 key
while (node != null) {
if (node.hash == hash &&
(node.key == key || key.equals(node.key))) {
// 找到相同 key,更新 value
return node.setValue(value);
}
node = node.next;
}
// 5. 未找到,插入新节点
addNode(hash, key, value);
return null;
}
}
2. 查找过程的两步验证
public class HashMapDemo {
public static void main(String[] args) {
Map<Person, String> map = new HashMap<>();
Person p1 = new Person("张三", 25);
Person p2 = new Person("张三", 25);
map.put(p1, "员工1");
// 查找过程:
// 1. 先比较 hashCode:p2.hashCode() == p1.hashCode()
// 2. 再比较 equals:p2.equals(p1)
// 两者都为 true 才认为是同一个 key
System.out.println(map.get(p2)); // 输出:员工1
}
}
class Person {
private String name;
private int age;
@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);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
违反契约的后果
1. 只重写 equals,不重写 hashCode
public class BadPerson {
private String name;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BadPerson that = (BadPerson) o;
return Objects.equals(name, that.name);
}
// 未重写 hashCode,使用 Object 的默认实现(基于内存地址)
}
public class Test {
public static void main(String[] args) {
BadPerson p1 = new BadPerson("张三");
BadPerson p2 = new BadPerson("张三");
System.out.println(p1.equals(p2)); // true
// 问题:HashMap 无法正确工作
Map<BadPerson, String> map = new HashMap<>();
map.put(p1, "员工1");
System.out.println(map.get(p2)); // null(期望是"员工1")
// 原因:p1 和 p2 的 hashCode 不同,定位到不同的桶
System.out.println(p1.hashCode()); // 例如:123456
System.out.println(p2.hashCode()); // 例如:789012
}
}
2. hashCode 不一致导致的问题
public class MutableKeyDemo {
public static void main(String[] args) {
Map<MutablePerson, String> map = new HashMap<>();
MutablePerson p = new MutablePerson("张三", 25);
map.put(p, "员工1");
// 修改对象状态,导致 hashCode 变化
p.setAge(26);
// 无法找到之前存入的值(hashCode 变了,定位到错误的桶)
System.out.println(map.get(p)); // null
// HashMap 内部出现"内存泄漏"(无法访问的键值对)
}
}
正确实现 hashCode 的原则
1. 使用 Objects.hash() 工具方法
public class Person {
private String name;
private int age;
private String email;
@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(email, person.email);
}
@Override
public int hashCode() {
// 推荐:使用 Objects.hash()
return Objects.hash(name, age, email);
}
}
2. 手动实现高质量 hashCode
public class Person {
private String name;
private int age;
@Override
public int hashCode() {
int result = 17; // 初始值(非零质数)
// 每个字段乘以质数(通常用 31)再累加
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + age;
return result;
}
}
为什么选择 31?
- 31 是质数,减少哈希冲突
31 * i == (i << 5) - i,JVM 可优化为位运算- 经验值,广泛应用于 JDK 源码
性能优化考量
1. 缓存 hashCode(不可变对象)
public final class ImmutablePerson {
private final String name;
private final int age;
private int hashCode; // 缓存 hashCode
@Override
public int hashCode() {
int result = hashCode;
if (result == 0) { // 延迟计算
result = 17;
result = 31 * result + name.hashCode();
result = 31 * result + age;
hashCode = result;
}
return result;
}
}
2. 避免使用可变字段
public class GoodPerson {
private final String id; // 不可变字段
private String name; // 可变字段
private int age; // 可变字段
@Override
public int hashCode() {
// 只使用不可变字段计算 hashCode
return Objects.hash(id);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GoodPerson that = (GoodPerson) o;
return Objects.equals(id, that.id);
}
}
实际案例分析
public class HashCollisionDemo {
public static void main(String[] args) {
// 案例1:String 的哈希冲突
Set<String> set = new HashSet<>();
set.add("Aa");
set.add("BB");
System.out.println(set.size()); // 2(虽然 hashCode 相同,但 equals 不同)
// 案例2:Integer 的 hashCode 就是其值
Integer i1 = 100;
Integer i2 = 100;
System.out.println(i1.hashCode() == i2.hashCode()); // true
System.out.println(i1.equals(i2)); // true
// 案例3:自定义对象
Person p1 = new Person("张三", 25);
Person p2 = new Person("李四", 25);
// 如果 hashCode 只基于 age,则 hashCode 相同但 equals 为 false
}
}
答题总结
面试要点:
- 直接回答:hashCode 相同,equals 不一定为 true,这是哈希冲突
- 契约关系:
equals 为 true → hashCode 必须相同(强制)hashCode 相同 → equals 不一定为 true(允许冲突)
- 原因分析:hashCode 是 int 类型(有限),对象数量无限,必然冲突
- HashMap 应用:先用 hashCode 定位桶,再用 equals 遍历链表查找
- 最佳实践:
- 重写 equals 必须重写 hashCode
- 使用
Objects.hash()或遵循 31 倍累加算法 - 不可变对象可缓存 hashCode
- 避免使用可变字段计算 hashCode
记忆口诀:
- equals 相等,hashCode 必相等
- hashCode 相等,equals 不一定
- 重写 equals,别忘 hashCode