这个并不是一个通用性编程问题,只属于Java领域内对类对比操作的专有问题。
这其实是个复杂问题,涉及到一些底层,也体现了Java语言的设计者是如何看待类的对比操作。
复杂是因为如下几个概念很相似却又不同:
- 类
- 实例
- 实例引用地址
- 不同属性的实例
看待这个问题,建议从宏观的角度看待会比较明朗。
equals()为什么要改写?
在超级父类 Object 中已经写好了 equals() 方法,所有继承他的类,都可以用这个方法比较两个实例的引用地址是否相同。
public boolean equals(Object obj) {
return (this == obj);
}
什么是引用地址?看代码介绍:
public class AddressDemo {
public static void main(String[] args) {
// 每次 new 都会创建新对象,获得新地址
Person p1 = new Person("Alice");
Person p2 = new Person("Alice"); // 又一个新对象!
Person p3 = p1; // 指向同一个对象
System.out.println("p1 == p2: " + (p1 == p2)); // false
System.out.println("p1 == p3: " + (p1 == p3)); // true
System.out.println("p2 == p3: " + (p2 == p3)); // false
System.out.println("p1.equals(p2): " + (p1.equals(p2))); // false
System.out.println("p1.equals(p3): " + (p1.equals(p3))); // true
System.out.println("p2.equals(p3): " + (p2.equals(p3))); // false
}
}
class Person {
String name;
public Person(String name) { this.name = name; }
}
一般来说equals都够用,但是一些业务流程需要这样处理:
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 没有重写 equals,使用 Object 的默认实现
}
// 测试
Person p1 = new Person("张三", 25);
Person p2 = new Person("张三", 25); // 内容完全相同,但是不同对象
System.out.println(p1.equals(p2)); // false ❌
System.out.println(p1 == p2); // false
想要p1对比p2,如果类的实例内容相同,就认为他们是同样的对象,即使他们的地址不同。
那就需要子类改写equals()了。
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
// 比较内容而不是引用地址
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return age == person.age && Objects.equals(name, person.name);
}
}
// 测试
Person p1 = new Person("张三", 25);
Person p2 = new Person("张三", 25); // 内容相同
System.out.println(p1.equals(p2)); // true ✅
System.out.println(p1 == p2); // false
那为什么又要连hashCode一起改写?
@IntrinsicCandidate //改标记的方法表明hashCode() 实现确实是一个 native 方法,它的具体实现细节因 JVM 而异。
public native int hashCode();
随便举例一个 JVM hashCode 的实现方式:
// 策略 :函数地址 + 随机数
hashCode = (object_address ^ random_seed);
用代码看看默认的hashCode效果:
public class AddressDemo {
public static void main(String[] args) {
// 每次 new 都会创建新对象,获得新地址
Person p1 = new Person("Alice");
Person p2 = new Person("Alice"); // 又一个新对象!
Person p3 = p1; // 指向同一个对象
System.out.println("p1.hashCode(): " + (p1.hashCode())); // 1854731462
System.out.println("p2.hashCode(): " + (p2.hashCode())); // 2036368507
System.out.println("p3.hashCode(): " + (p3.hashCode())); // 1854731462
}
}
class Person {
String name;
public Person(String name) { this.name = name; }
}
可以看出默认的hashCode和默认的equals所输出的结果是一致性的。
那回归我们的问题,为什么我们的自定义的子类改写了equals,需要连hashCode一起改写?
如果你的子类需要在HashMap、HashSet等类使用的时候(使用对比相等性操作),会发生行为异常!
如果你的类需要在集合中使用,HashMap、HashSet等类会调用hashCode()去作为对比的参考。
比如下面的问题——找不到值:
import java.util.*;
public class Test {
public static void main(String[] args) {
Map<Person, String> map = new HashMap<>();
Person p1 = new Person("Alice", 25);
Person p2 = new Person("Alice", 25); // 业务逻辑上认为是同一个人
map.put(p1, "经理");
// 问题来了!
System.out.println(map.get(p2)); // 输出:null
// 虽然 p1 和 p2 内容相同,但哈希值不同,所以找不到!
}
}
还有,重复值问题:
Set<Person> set = new HashSet<>();
set.add(new Person("Bob", 30));
set.add(new Person("Bob", 30)); // 逻辑上应该去重
System.out.println(set.size()); // 输出:2 - 竟然有两个!
这就解释了,为什么必须同时重写 equals 和 hashCode 方法,因为你修改了equals改变了对比实例的逻辑,但是hashCode还是默认的逻辑,在其他对比方法调用的时候就会引发BUG、风险、漏洞、灾难!
这也是Java希望使用者遵循规则:相等的对象必须有相等的哈希值。
完整的配套重写案例:
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
// 现在可以正常工作了
System.out.println("集合中是否包含李四: " + personSet.contains(p2));
// 输出:true ✅
最后,我们来看看官方在代码中给予的建议:
/**
* Indicates whether some other object is "equal to" this one.
*
* @apiNote
* It is generally necessary to override the `hashCode` method whenever this
* method is overridden, so as to maintain the general contract for the `hashCode`
* method, which states that equal objects must have equal hash codes.
*/
public boolean equals(Object obj) {
return (this == obj);
}
总结
感觉这两个方法设计得比较耦合,为后续的不理性使用埋下了灾难的风险。
所以对equals或hashCode任意一个方法重写时,都需要配套的一起都改写了,但一起改写不是目的,他们返回的逻辑的一致性才是最终目的。
浙公网安备 33010602011771号