【Effective Java 10.3】覆盖 equals 时请遵守通用约定 —— 传递性
1. 传递性要求
equals
约定的第三个要求是,如果 x eq y,y eq z,则 x eq z。
我们很容易在无意识的情况下违反这条约定。特别是 “值” 类组件的超类中,子类对超类 equals
行为的扩展导致 equals
函数的传递性问题。
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 o) {
if (!(o instanceof Point)) {
return false;
}
Point p = (Point) o;
return p.x == this.x && p.y == this.y;
}
}
例如,对于 Point
类,如果我们要扩展一个 Point
的子类 ColorPoint
。如果值。
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
// 没有覆写 equals 方法, 而是直接从 Point 继承过来,在比较时颜色信息会被忽略掉
}
如果要在 ColorPoint
中覆写 Point
的 equals 方法,让其在比较时不忽略颜色信息。
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override
public boolean equals(Object o) {
// 如果 o 不是一个 Point 对象
if (!(o instanceof Point)) {
return false;
}
// 如果 o 是一个 Point 对象但不是一个 ColorPoint 对象
// 则无视 color 域进行比较
if (!(o instanceof ColorPoint)) {
return o.equals(this);
}
// o is a ColorPoint; do a full comparison
return super.equals(o) && ((ColorPoint) o).color.equals(color);
}
}
进行如上修改后,ColorPoint
会在进行 “混合比较” 时忽略颜色信息。这种做法提供了对称性,但牺牲了传递性。虽然没有一种令人满意的方法可以既扩展不可实例化的类,又增加组件值,但还是有一种权宜之计,即 “组合由于继承”。不再让 ColorPoint
继承 Point
,而是在 ColorPoint
中加入一个私有的 Point
域,以及一个公有视图(view)方法,此方法返回一个承担将 ColorPoint
转换为 Point
的任务。
public class ColorPoint {
private final Color color;
private final Point point;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
// 返回 ColorPoint 的 Point 视图,承担修改前的 (Point) colorPoint 的任务
public Point asPoint() {
return point;
}
public boolean equals(Object o) {
if (!(o instanceof ColorPoint)) {
return false;
}
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(this.point) && cp.color.equals(color);
}
}
在实际开发中,对于值类型的对象的equals比较,最好只在同类对象之间进行,尽量不要再超类与子类,子类与子类之间进行 equals 操作
2. Jdk 中一些违反传递性原则的例子
Java 平台类库中,有一些扩展了可实例化的类,并添加了新的值组件。例如,java.sql.Timestamp
对 java.util.Date
进行了扩展,并添加了 nanoseconds
域。Timestamp
的实现的确违反了对称性与传递性。因此,其有一个免责声明。