Java中hashCode方法与equal方法何时重写
背景:项目中遇到需要去重的类实体,使用集合HashSet,需要在该类中重写hashCode与equal方法,了解一下Object中的这两个方法。
在 Java 中,hashCode()
和 equals()
方法通常需要一起重写,特别是当你创建自定义类并希望该类的对象能够在基于哈希的集合(如 HashMap
、HashSet
、LinkedHashMap
、LinkedHashSet
)中正确工作时。
1. 为什么需要重写这两个方法?
equals()
:默认实现(Object.equals()
)比较的是对象的引用(内存地址),而大多数情况下,我们希望比较对象的内容是否相等。hashCode()
:哈希集合(如HashMap
)依赖hashCode()
来确定对象在哈希表中的存储位置。如果两个对象通过equals()
比较相等,但hashCode()
返回不同的值,会导致哈希集合无法正常工作(例如,无法正确存储或查找元素)。
2. 何时需要重写?
2.1 当你需要自定义对象的相等性逻辑时
- 示例场景:
- 比较两个
Person
对象是否相等,只要它们的id
相同即认为相等。 - 比较两个
Point
对象(表示坐标点)是否相等,只要x
和y
坐标相同即认为相等。
- 比较两个
2.2 当你的类会作为哈希集合的键时
- 必须同时重写
equals()
和hashCode()
,确保:- 一致性:如果两个对象
equals()
相等,则它们的hashCode()
必须相同。 - 稳定性:对象的
hashCode()
在其生命周期内不应改变(通常基于不可变字段计算)。
- 一致性:如果两个对象
3. 如何正确重写?
3.1 重写 equals()
的原则
- 自反性:
x.equals(x)
必须为true
。 - 对称性:
x.equals(y)
为true
⇒y.equals(x)
也为true
。 - 传递性:若
x.equals(y)
和y.equals(z)
为true
,则x.equals(z)
也为true
。 - 一致性:多次调用
x.equals(y)
结果应相同(前提是对象未改变)。 - 非空性:
x.equals(null)
必须为false
。
例如:
@Override
public boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Person person = (Person) o;return id == person.id; // 假设通过id判断相等性
}
3.2 重写 hashCode()
的原则
- 相等性约束:若
x.equals(y)
为true
,则x.hashCode() == y.hashCode()
必须成立。 - 散列均匀性:尽量让不同的对象返回不同的哈希值,减少哈希冲突。
例如:
@Override
public int hashCode() {int result = 17; // 一个非零的初始值result = 31 * result + (field1 != null ? field1.hashCode() : 0);result = 31 * result + (field2 != null ? field2.hashCode() : 0);return result;
}
4. 常见误区与注意事项
-
只重写
会导致哈希集合(如equals()
而不重写hashCode()
:HashMap
、HashSet
)无法正常工作。例如:Person p1 = new Person(1, "Alice"); Person p2 = new Person(1, "Alice"); Set<Person> set = new HashSet<>(); set.add(p1); set.contains(p2); // 返回false(即使p1和p2通过equals()相等)
-
使用 IDE 自动生成方法:
- 大多数 IDE(如 IntelliJ、Eclipse)可以自动生成
equals()
和hashCode()
,基于对象的字段计算。
- 大多数 IDE(如 IntelliJ、Eclipse)可以自动生成
-
不可变类(如
String
、Integer
):- 这些类已经正确重写了
equals()
和hashCode()
,因此可以直接用作哈希集合的键。
- 这些类已经正确重写了
总结
- 必须重写:当你的类需要自定义相等性逻辑,或作为哈希集合的键时。
- 成对重写:重写
equals()
时必须同时重写hashCode()
,确保两者一致性。 - 使用工具:利用 IDE 或 Lombok 等工具自动生成方法,减少手动错误。
扩展知识:
hashCode方法重写时使用31作为乘数的原因主要包括以下几点:
-
奇质数的特性:31是一个奇质数,这意味着它能有效地减少哈希冲突的概率。使用质数作为乘数可以帮助分散哈希值,从而提高哈希表的性能12。
-
位运算效率:在计算机中,乘以31可以通过位运算来优化,具体为
(x << 5) - x
。这种方式比直接乘法更加高效,因为位移操作通常比乘法快得多12。 -
良好的分布性:经过实践证明,31可以提供良好的哈希值分布,适用于字符串等对象的哈希计算。它能够有效地将不同的输入映射到不同的哈希值上,减少了碰撞的可能性12。
-
历史原因和测试结果:31作为一个不大不小的质数,经过大量测试表明其在哈希计算中表现良好,冲突率较低。例如,使用31、33、37、39和41作为乘数进行哈希计算时,31的冲突结果最少4。