覆盖equals方法须要遵守通用约定:
这里需要重点关注的情况是: 子类继承父类并且扩展了新的比较字段时,稍有不慎就会违反对称性或者传递性。
一般大部分equals都是这样实现的:
class Point {
private final int x;
private final int y;
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 == x && p.y == y;
}
}
如图,如果是 父类.equals(子类) 的情况,父类是看不到字段3的,这时如果进行比较一定是返回 true, 但是反过来 子类.equals(父类) 有两种情况:
第一种情况是返回 false, 这种情况就违反了对称性。
第二种情况是返回 true,这种情况符合对称性,但是违反了传递性
java库中的java.sql.Timestamp类则是扩展了java.util.Date类并添加了nanoseconds扩展字段,在类的文档中则是添加了这块的免责声明:
当前可以得出第一个结论:如果父类是可实例化类并且子类可以重写equals方法,只要父类中的equals是用instanceof的方式实现的,则子类扩展了新的值组件就会违反对称性或传递性,除非子类不扩展新的值组件。这里强调父类是可实例化类,假如父类是抽象类,因为无法实例化抽象类,因此子类扩展了值组件就不会出现上面出现的问题。
一种解决方式是将equals设为final,这样子类就无法覆盖equals方法自然就不会存在上述问题,但是这样做有些极端,因为子类覆盖equals不一定是要添加新的用于比较的值组件,比如改善equals的性能。
还有另一种实现思路:
class Point {
private final int x;
private final int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if (o == null || o.getClass() != getClass()) {
return false;
}
Point p = (Point) o;
return p.x == x && p.y == y;
}
}
这样实现可以解决对称性和传递性, 但是对于一个子类而言即使没有扩展新的值组件也无法和父类比较了。在面向对象编程中 子类 IS A 父类,因为人们第一感觉认为一个子类对象和父类对象之间的比较是很自然的。因此很多情况下这种结果并不能够接受。
综合以上所述:可以得出一个综合性的结论:
无法在扩展可实例化类的同时,既增加新的值组件,同时又保留equals约定,除非愿意放弃面向对象的抽象所带来的优势。
有一个权宜之计就是不用继承,用聚合来实现,,扩展类和基础类之间没有继承关系,上面的问题都得到解决。
总之关于这个问题没有一个完美的实现方式,实际解决问题时只要清楚上述问题再综合权衡使用适合的方式。
参考资料:
Effective Java 第三版