《Effective Java》笔记之重写equals时要遵守的通用约定

时间:2021-10-07 16:15:22

最近在阅读鼎鼎大名的《Effective Java》,想把一些自己觉得比较经典的内容以笔记形式记录在博客中,这是第一篇,接下来一段时间会持续更新笔记。

重写quals方法看起来很简单,但是不正确的重写方式可能会导致错误,并且后果非常严重还不容易找到问题所在。

首先,要明白什么时候需要重写equals,什么时候不需要。

不需要重写equals的情况:
1。类的每个实例本质上都是唯一的。比如代表活动实体而不是值的类,像Thread。
2.不关心类是否提供了“逻辑相等”的测试功能。比如java.util.Random类,设计者并不认为用户需要比较两个Random对象需要比较产生的随机数是否相等的功能,所以直接继承Object的equals就足够了。(不过Random还是重写了equals方法)
3.父类已经重写了equals,并且其行为也符合子类需求。

需要重写equals的情况:
类具体自己的“逻辑相等”概念,而且父类还没有重写equals实现期望的行为。比如希望比较两个对象是如果两个对象的成员变量都相等则认为两个对象相等,而不是两个引用指向同一个对象才相等。

重写equals方法的通用约定:

1.自反性:x != null的情况下,x.equals(x)必须返回true
2.x != null,y != null的情况下,当且仅当x.equals(y)返回true时,y.equals(x)必须返回true。
3.当x、y、z都不为null时,如果x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)也必须返回true。
4.一致性:对于非null的x、y,只要equals的比较操作在对象中所用的信息没有修被改,多次调用x.equals(y)必须返回一致的结果。
5.非空性:非null对象x.equals(null)一定返回false

如果没有遵守以上的约定,则程序可能表现不正常甚至奔溃,并且难以发现问题的根源。最常见就是集合类对元素的判断都依赖于元素是否遵守了equals的约定。

以下看看违背约定的一些典型情形:
1.对于自反性,除非是故意违反,不然一般很难出现违反的情况。

2.对于对称性,这里举个例子:

这是一个表示不区分大小写字符串的类:


public class CaseInsensitiveString {
private String s;

public String getS() {
return s;
}

public CaseInsensitiveString(String s) {
if (s == null) {
throw new NullPointerException();
}
this.s = s;
}

public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString) {
return s.equalsIgnoreCase(((CaseInsensitiveString)o).getS());
}
if (o instanceof String) {
return s.equalsIgnoreCase((String) o);

}
return false;
}

}

如果使用以下代码:

CaseInsensitiveString cis = new CaseInsensitiveString("polish");
String s = "polish";

System.out.println(s.equals(cis));
System.out.println(cis.equals(s));

则得出的结果是:
false
true

很明显,违背了对称性原则。这将导致比如:

ArrayList<CaseInsensitiveString> list = new ArrayList<>(cis );
list .add();
list .contains(s)

结果可能会返回true甚至抛出异常。

为了解决这个问题,只要不让CaseInsensitiveString和String交互就可以:

public boolean equals(Object o) {
return o instanceof CaseInsensitiveString&&s.equalsIgnoreCase(((CaseInsensitiveString)o).getS());
}

3.对于传递性

现在有个类表示一个二维空间的点:


public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}

public boolean equals(Object o) {
if (!(o instanceof Point)) {
return false;
}
Point p = (Point)o;
return p.x == x&&p.y == y;
}

}

现在为了表示有颜色的点扩展这个类:

public class ColorPoint extends Point{
private Color color;
public ColorPoint(int x, int y,Color color) {
super(x, y);
this.color = color;
}

public boolean equals(Object o) {
if (!(o instanceof ColorPoint)) {
return false;
}
return super.equals(o)&&((ColorPoint)o).color == color;
}

}

此时假如执行:

ColorPoint cp = new ColorPoint(1, 2, Color.red);
Point p = new Point(1, 2);

System.out.println(cp.equals(p));
System.out.println(p.equals(cp));

结果为:
false
true

显然违背了对称性。

可以在比较的时候忽略颜色信息:

public boolean equals(Object o) {
if (!(o instanceof Point)) {
return false;
}
if (!(o instanceof ColorPoint)) {
return o.equals(this);
}
return super.equals(o)&&((ColorPoint)o).color == color;
}

但是这样提供了对称性,却牺牲了传递性。
假如:

ColorPoint p1 = new ColorPoint(1, 2, Color.red);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.black);

System.out.println(p1.equals(p2));
System.out.println(p2.equals(p3));
System.out.println(p1.equals(p3));

结果:
true
true
false

事实上,这是面向对象只能怪关于等价关系的一个基本问题:我们无法再扩展可实例化的类的同时,既添加新的值组件,又保留equals的约定。

4.一致性
可变对象可以再不同时候与不同的对象相等,而不可变对象则相等的对象永远相等不相等的对象永远不相等。

注意,不要使equals方法依赖不可靠的资源,比如可能发生改变的资源。

5.对于非空性而言,一般无需做空判断,直接调用instanceOf判断类型就可以默认完成空判断。