覆盖 equals 时请遵守通用约定
覆盖 equals 方法看似很简单,但是有很多覆盖方式会导致错误。equals也算是我们在开发中最常使用到的方法了。但是我们平常都很少会去重写equals
方法。在这种情况下,类的每个实例都只与它本身相等。
什么时候覆盖 equals 方法?
如果类具有自己特有的 “逻辑相等”,并且超类还没有覆盖 equals,通常属于值类,这种做法在 String 类中最常见,因为
想要两个字符串对比,并不能单纯的比对两者的内存地址是否相同,而是要判断两个字符串中的内容是否完全一致。
为什么在 Java 中 String 类的 equals 方法要被重写?直接使用 Object 的 equals 方法不行吗?
答案是需要区分情况的。
1、字符串常量的情况:
String s1 = "abc";
String s2 = "abc";
对字符串常量来说,Java 会有字符串常量池,在 JVM 运行时分配的内存区域,确保该池中不会存在两个内存相同但内存地址不同的字符串对象。
对于上述这种情况,无论是否重写 equals 方法,使用 Object 的 equals 方法进行比较都不会出现问题。
2、使用 new 对象的方式来创建字符串
String s1 = new String("abc");
String s2 = new String("abc");
对于这种情况,使用 new 关键字创建的字符串对象,它们是会在堆中创建对象。这里只能做假设,或者自行修改 JDK 源码,去掉 String 的 equals 方法。想必使用 Object 的 equals 方法理论上是上述两个对象进行比对,返回的是 false。
这两种情况的结果就有了冲突,虽然大部分情况下,我们使用 String 都基本不会用 new 的方式去创建字符串对象。但为了普适性,JDK开发者还是做了考虑,直接重写 equals 方法,就没有那么多麻烦了。
但是,有一种值类不需要覆盖 equals 方法,书中所说的 “实例受控” 确保 “每个值至多只存在一个对象的类”,第一映像想到的就是单例模式,也包括书中所讲的枚举,对于这样的类来说,Object 的 equals 方法完全符合这种类的使用逻辑,所以不需要重写。
如何覆盖 equals 方法?
在覆盖 equals 方法时,也必须要遵守它的通用约定,下面是 Object 的规范
- 自反性:对于任何非 null 的引用值 x,x.equals(x) 必须返回 true。讲人话就是同一个对象进行 equals 的比对,必须是一样的,返回 true
Object a = new Object();
boolean result = a.equals(a);
System.out.println(result);// result 必须是 true
- 对称性:对于任何非 null 的引用值 x 和 y,当 y.equals(x) 返回 true,x.equals(y) 也必须返回 true,可以看下面案例
public class Person implements Cloneable {
private Address address;
// 省略构造函数、Getter&Setter方法
@Override
public Person clone() {
try {
Person person = (Person) super.clone();
return person;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
public class Address implements Cloneable{
private String name;
// 省略构造函数、Getter&Setter方法
@Override
public Address clone() {
try {
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException, IOException, ClassNotFoundException {
Person person1 = new Person(new Address("测试"));
// 克隆一个 person 对象
Person person2 = person1.clone();
Address address1 = person1.getAddress();
Address address2 = person2.getAddress();
System.out.println(address1.equals(address2)); // 输出 true
System.out.println(address2.equals(address1)); // 输出 true
}
}
- 传递性:对于任何非 null 的引用值 x、y和z,简单说就是 x 、y 、z哪个对象对另一个对象进行 equals 方法,都必须返回 true
- 一致性:对于任何非 null 的引用值 x 和 y,只要 equals 的比较操作在对象中所用的信息没有被修改,多次调用 x.equals(y) 就需要一致的返回 true 或者 false
- 对于任何不是 null 的引用 x,x.equals(null) 必须返回false
最后的告诫
由于书中内容较长,只取了实用的部分。
不要将 equals 声明中的 Object 对象替换成其他的类型
public class Address implements Cloneable {
private String name;
@Override
public boolean equals(Object obj) {
return super.equals(obj);
}
public boolean equals(Address o) {
return Objects.equals(this, o);
}
// 省略构造函数、Getter&Setter方法
@Override
public Address clone() {
try {
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
上面这种写法是可以编译成功并且启动也不会报错,两个 equals 方法。问题在于equals(Address o)
并没有覆盖 Object 的方法,因为它的参数应该是 Object 类型,并且该方法也没有 @Override 注解,如果加上该注解方法会编译错误的。
以上是一种极端情况,在现在的开发中,已经不需要手动去编写 equals 方法。例如可以使用 Lombok 的 @EqualsAndHashCode
注解,就会帮我们生成 equals方法,减少人工编写代码遇到的错误
总结
我们在开发中也基本上不会遇到需要覆盖 equals方法,除非迫不得已。在很多情况下,Object 的 equals 方法就已经可以覆盖到大部分的使用场景了。
参考文献
-
ChatGPT