第三章 对于所有对象都通用的方法
第十条:覆盖equals时请遵守通用约定
10.1 什么情况下不用覆盖equals
-
类的每个实例本质上都是唯一的。关注的实体本体还不是里面的值的类。例如
Thread
。 -
类没有必要提供“逻辑相等(logical equality)”的测试功能。例如
java.util.regex.Pattern
可以覆盖equals,已检查两个Pattern实例是否代表同一个正则表达式。(设计者认为程序员不需要这样的功能) -
超类已经覆盖了equals,超类的行为对于这个类也是合适的。例如大多数
Set
实现了都从AbstractSet继承equals实现。 - 类是私有的,或者包是私有的,可以确定它的equals方法永远都不会被调用。但是如果你非要规避风险,可以覆盖equals方法,以确保它不会被意外调用:
/**
* @author yuxingang
* date 2020-12-24 09:29
*/
public class Equals {
private class DonWantEqual{
@Override
public boolean equals(Object obj) {
System.out.println("1111");
throw new AssertionError("断言异常");//Method is never called
}
}
}
断言:对参数和环境等做出判断,避免程序因不当的输入或错误的环境而产生逻辑异常。
简单来说:断言主要使用在代码开发和测试时期,用于对某些关键数据的判断,如果这个关键数据不是你程序所预期的数据,程序就提出警告或退出。
10.2 什么情况下应该覆盖equals
如果类具有逻辑相等(logical equality)
概念,而且超类还没有覆盖equals
。这通常属于“值类(value class)”。值类仅仅是一个表示值的类,例如:Integer或者String。就必须覆盖equals。而且这样做也使得这个类的实例可以被用作映射表(map)的键(key),或者集合(set)的元素,使得映射或者集合表现出预期的行为。
10.2.1 特殊情况
有一种值类,不需要覆盖equals方法,即实例受控(静态工厂提供的类或者枚举类型等)
确保“每个字至多只存在一个对象”的类。对于枚举类型,逻辑相同和对象等同是一回事。因此没必要重写equals。
10.2.2 重写equals规范
必须遵守以下通用约定:
equals方法实现了等价关系
,其属性如下:
- 自反性:对于任何非null的引用值x,x.equals(x)必须返回true
- 对称性:对于任何非null的引用值x和y,如果x.equals(y)返回true,那么y.equals(x)返回true
- 传递性:对于任何非null的引用值x,y,和z。如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)返回true
- 一致性:对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有修改,多次调用x.equals(y)返回值必须一样
- 对于任何非null的引用值x,x.equals(null)必须返回false。
10.3 违反equals重写规范的可能性
10.3.1 自反性
基本上不可能违反,除非你秀逗了
10.3.2 对称性
示例:如下,它实现了区分大小写的字符串。字符串由toString保存,但在equals操作中被忽略。
/**
* @author yuxingang
* date 2020-12-24 10:45
*/
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s){
this.s = Objects.requireNonNull(s);
}
//Broken - violates symmetry!
@Override
public boolean equals(Object obj) {
if(obj instanceof CaseInsensitiveString)
return s.equalsIgnoreCase(((CaseInsensitiveString) obj).s);
if(obj instanceof String)//One-way interoperability!
return s.equalsIgnoreCase((String)obj);
return false;
}
}
测试:
CaseInsensitiveString cis = new CaseInsensitiveString("YuXinGang");
String s = "yuxingang";
System.out.println("正向比较cis?s===>"+ cis.equals(s));
System.out.println("反向比较cis?s===>"+s.equals(cis));
结果:
结论:问题在于上面的equal方法左边的会进行大小写处理,但是右边是不会的,它只是一个参数。所以在s.equals(cis),中s本身就是小写,在进行大小写处理的时候还是小写,而右边的参数是存在大小写的,所以返回false。一旦违反了equals约定,当其它对象面对你的对象时,你完全不知道这些对象的行为会怎么样。
解决方案:
@Override
public boolean equals(Object obj) {
return obj instanceof CaseInsensitiveString && ((CaseInsensitiveString)obj).s.equalsIgnoreCase(s);
}
10.3.3 传递性
示例:假设讲一个新的值组件添加到超类中,换句话说,子类增加的信息会影响equals的比较结果。如下是一个不可变的二维整数型Point类
Point类
/**
* @author yuxingang
* date 2020-12-24 10:45
*/
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Point))
return false;
Point p = (Point) obj;
return p.x == y && p.y == y;
}
}
这个时候想要扩展这个类,想加点颜色信息
ColorPoint类
/**
* @author yuxingang
* date 2020-12-24 11:31
*/
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
}
如果完全不提供equals方法,那么颜色信息就会被忽略掉。尝试第一种在子类的equals实现
//Broken -violates symmetry!
@Override
public boolean equals(Object obj) {
if(!(obj instanceof ColorPoint))
return false;
return super.equals(obj)&&(((ColorPoint) obj).color)==color;
}
问题所在:在比较普通点和有色点,以及相反的情形时,可能会得到不同的结果。普通点.equals(有色点)直接忽略了颜色信息。而相反的,由于参数类型不一致,所以永远都是false
Point p = new Point(1,2);
ColorPoint cp = new ColorPoint(1,2,ColorPoint.RED);
如上,p.equals(cp)返回true。cp.equals§返回false。我们可以在ColorPoint.equeals在进行“混合比较”时忽略颜色信息。
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Point))
return false;
//if obj is a normal Point,do a color-blind comparison
if(!(obj instanceof ColorPoint))
return super.equals(this);
//obj is a ColorPoint; do a full comparison
return super.equals(obj)&&((ColorPoint)obj).color==color;
}
ColorPoint p1 = new ColorPoint(1,2,Color.RED);
Point p2 = new Point(1,2);
ColorPoint p3 = new ColorPoint(1,2,Color.BLUE);
此时,p1.equals(p2)和p2.equals(p1)返回true,但是p1.equals(p3)则返回false,这虽然保证了对称性,但是违反了传递性。前两种没有考虑颜色,但是后一种把颜色算了上去。并且这还可能会导致无限递归的可能性。例如有Point有2个子类,如果各自都带有上面的方法,就会抛*Error
异常。
10.3.4 一致性
如果两个对象相等,它们就必须使用保持相等,除非它们中有对象被修改了。所以先判断对象是否可变,如果不可变,那必须的保证两个对象equals方法必须相等。
10.3.5 非空性
所有的对象都不能等于null
@override
public boolean equals(Object obj){
if(obj == null)
return false;
....
}
以上if测试是不必要的。为了测试其参数的等同性,equals方法必须先把参数转化成适当的类型,以便可以调用它的方法或成员变量。在进行转化之前,equals必须使用instanceof操作符,检查其参数是否是该类的对象或子类对象。
如果漏掉了instanceof检查,并且传递给equals方法的参数又是错误类型,那么equals方法将会抛出ClassCastException
异常,这就违反了equals的约定。但是,如果instanceof的第一个操作数为null,那么,不管第二个操作数是什么类型,instanceof操作符都指定应该返回false,因此不需要单独的null检查,而应该用instanceof。
10.4 实现高质量equals的诀窍
1、“使用==操作符检查“参数是否为这个对象的引用”。如果是则返回true。这是一种性能优化
if(this==obj)
return true;
2、“使用instanceof操作符检查“参数是否为正确的类型”,如果不是则返回false。所谓的正确的类型是指equals方法所在的那个类,或者是该类的父类或接口
3、把参数转化成正确的类型:因为上一步已经做过instanceof测试,所以确保转化会成功
4、对于该类的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配(其实就是比较两个对象的值是否相等了)
5、当你编写完equals方法之后,应该问自己三个问题:它是否是对称的
、传递的
、一致的
?当然equals也必须满足其他两个约定(自反性、非空性)但是这两种约定通常会自动满足。
10.5 使用equals的注意点
- 覆盖equals时总要覆盖hashCode(见第9条)
- 不要企图让equals方法过于智能:不要想过度地去寻求各种等价关系,否则容易陷入各种麻烦
- 不要将equals声明中的Object对象替换为其他类型,不然就不是重写equals了,而是重载了。加上@override可以避免这种错误发生
- 使用Coogle开源的AutoValue框架,会自动生成这些方法。通过类的注释就能触发。这个框架比IDEA的好,因为IDEA的冗长,可读性差,无法自动追踪类的变化,因此需要测试。
总结:不要轻易覆盖equals,如果覆盖,一定要比较这个类的所有关键域,并且查看它们是否遵守equals合约的五项条款