在Java开发中,我们经常会遇到需要比较两个对象是否相等的情况。初学者常常会困惑:为什么重写了equals方法后,还必须重写hashCode方法?这要从Java对象比较的底层机制说起。
Java中的对象比较分为两种:
当我们使用HashMap、HashSet等基于哈希表的集合时,对象的hashCode值决定了它会被放入哪个"桶"(bucket)中。而equals方法则用于在同一个桶内进一步比较对象是否真正相等。
重要原则:如果两个对象通过equals比较返回true,那么它们的hashCode值必须相同。反之则不一定成立。
这个原则被称为"hashCode契约",违反它会导致集合类无法正常工作。比如把对象存入HashSet后,可能无法通过contains方法正确找到。
一个符合规范的equals方法实现需要满足以下特性:
以下是一个标准的equals方法实现模板:
java复制@Override
public boolean equals(Object o) {
// 1. 检查是否是同一个对象
if (this == o) return true;
// 2. 检查是否为null或类型不匹配
if (o == null || getClass() != o.getClass()) return false;
// 3. 类型转换
MyClass myClass = (MyClass) o;
// 4. 逐个比较关键字段
return field1 == myClass.field1 &&
Objects.equals(field2, myClass.field2) &&
// 其他字段比较...
}
实际开发中常见的坑:
hashCode的主要作用是:
一个好的hashCode实现应该:
Java 7以后推荐使用Objects.hash():
java复制@Override
public int hashCode() {
return Objects.hash(field1, field2, field3 /* 所有equals中使用的字段 */);
}
对于性能敏感的场景,可以考虑手动计算:
java复制@Override
public int hashCode() {
int result = field1 != null ? field1.hashCode() : 0;
result = 31 * result + (field2 != null ? field2.hashCode() : 0);
// 其他字段...
return result;
}
为什么选择31作为乘数?
最常见的症状:
这些通常都是因为:
对于频繁用作Map键的不可变对象:
java复制private int hashCode; // 默认为0
@Override
public int hashCode() {
if (hashCode == 0) {
hashCode = Objects.hash(field1, field2);
}
return hashCode;
}
注意:这种优化只适用于不可变对象!
现代IDE(IntelliJ IDEA/Eclipse)都提供equals和hashCode的自动生成功能。以IntelliJ为例:
生成的代码通常符合规范,但要注意:
使用@EqualsAndHashCode注解:
java复制@EqualsAndHashCode
public class MyClass {
private String field1;
private int field2;
// 其他字段...
}
可以指定包含/排除字段:
java复制@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class MyClass {
@EqualsAndHashCode.Include
private String keyField;
private String otherField; // 不会被包含
}
使用JUnit测试equals和hashCode契约:
java复制@Test
public void testEqualsContract() {
MyClass a = new MyClass("test", 1);
MyClass b = new MyClass("test", 1);
MyClass c = new MyClass("test", 1);
// 自反性
assertTrue(a.equals(a));
// 对称性
assertTrue(a.equals(b));
assertTrue(b.equals(a));
// 传递性
assertTrue(a.equals(b));
assertTrue(b.equals(c));
assertTrue(a.equals(c));
// 一致性
for (int i = 0; i < 100; i++) {
assertTrue(a.equals(b));
}
// 非空性
assertFalse(a.equals(null));
}
@Test
public void testHashCodeContract() {
MyClass a = new MyClass("test", 1);
MyClass b = new MyClass("test", 1);
// 如果equals返回true,hashCode必须相同
assertTrue(a.equals(b));
assertEquals(a.hashCode(), b.hashCode());
}
当存在继承关系时,equals和hashCode的实现需要特别小心。考虑这个例子:
java复制class Parent {
private String parentField;
@Override
public boolean equals(Object o) {
// 标准实现...
}
}
class Child extends Parent {
private String childField;
// 如何实现equals?
}
解决方案:
对于包含大量字段的对象,计算hashCode可能成为性能瓶颈。可以考虑:
除了手动实现,还可以考虑:
这些库方法通常:
String类已经正确实现了equals和hashCode:
java复制String s1 = "hello";
String s2 = new String("hello");
System.out.println(s1.equals(s2)); // true
System.out.println(s1.hashCode() == s2.hashCode()); // true
这是因为String的hashCode是基于字符串内容计算的:
java复制public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
枚举类型不需要手动实现equals和hashCode,因为:
java复制enum Color { RED, GREEN, BLUE }
Color c1 = Color.RED;
Color c2 = Color.RED;
System.out.println(c1.equals(c2)); // true
System.out.println(c1.hashCode() == c2.hashCode()); // true
对于包含数组字段的对象,需要使用Arrays.equals():
java复制class MyClass {
private int[] values;
@Override
public boolean equals(Object o) {
// ...
return Arrays.equals(values, other.values);
}
@Override
public int hashCode() {
return Arrays.hashCode(values);
}
}
普通数组的hashCode()方法不会基于内容计算,所以必须使用Arrays.hashCode()。