重写 equals 方法有许多的重写方式会导致错误,所以要么不重写 equals 方法,要么重写时就要尽力遵守通用约定。
可以不重写equals方法的情况
如果不重写equals方法,那该类的每个实例都只与它自身相等,而有时候这就是我们需要的。
1、类的每个实例本质上都是唯一的
对于代表活动实体(例如 Thread),而不是值(Value)的类来说确实如此,Object提供的equals实现对于这些类来说是正确的行为。
2、不关心类是否提供“逻辑相等”的测试功能
有些类我们只关注它提供的功能,而不是实例之间是否相等。比如java.util.Random类,客户一般用它生成随机数,基本不会检测两个Random生成的随机数是否相同,所以对于这些类重写equals方法并没有意义。
3、父类已经重写equals,从父类继承过来的行为对于子类也是合适的
这些在集合框架中比较常见,比如大多数的Set实现都从AbstractSet继承equals实现,List实现从AbstractList继承equals实现,Map实现从AbstractMap继承equals实现。
4、类是私有的或是包级私有的,可以确定它的equals方法永远不会被调用
这种情况下最好重写equals方法,以防它被意外调用,可以在重写equals方法中抛出异常。
@Override
public boolean equals(Object obj) {
throw new AssertionError();
}
重写equals方法的情景
如果类具有自己特有的“逻辑相等”概念(不同于对象等同概念),而且父类还没有重写equals以实现期望的行为,这时我们就需要重写equals方法,这通常属于”值类”的情形。
值类仅仅是一个表示值的类,例如Integer和Date,程序猿在利用equals方法来比较值对象的引用时,希望知道它们在逻辑上是否相等。而不是想了解它们是否指向同一个对象。
重写equals方法的通用约定
1、自反性(reflexive):对于任何非null的引用值x,x.equals(x)必须返回true。
2、对称性(symmetric):对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
3、传递性(transitive):对于任何非null的引用值x,y,z,如果x.equals(y)返回true,并且y.equals(z)返回true,那么x.equals(z)也必须返回true。
4、一致性(consistent):对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致地返回false。
5、对于任何非null的引用值x,x.equals(null)必须返回false。
违反对称性
举例:
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
if (s == null) {
throw new NullPointerException();
}
this.s = s;
}
@Override
public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString) {
return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
}
if (o instanceof String) {
return s.equalsIgnoreCase((String) o);
}
return false;
}
}
测试:
public static void main(String[] args) {
CaseInsensitiveString cis=new CaseInsensitiveString("Java");
String str="java";
System.out.println(cis.equals(str));
System.out.println(str.equals(cis));
}
输出:
true
false
当调用 cis.equals(str) 时,使用的是CaseInsensitiveString类的equals方法返回true;但是str.equals(cis)调用的是String类的equals方法返回false;String类是不知道cis是什么鬼,更不知道如何和cis进行比较,肯定返回false ,这就不满足对称性了。
解决这个问题的方法,把企图和String互操作的代码从equals中删掉就可以了。
@Override
public boolean equals(Object o) {
return o instanceof CaseInsensitiveString
&&((CaseInsensitiveString)o).s.equalsIgnoreCase(s);
}
违反传递性
定义二维整数型Point类:
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 == x && p.y == y;
}
}
之后由于需求,添加颜色信息,对Point类进行扩展:
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
}
此时如果对ColorPoint调用equals方法,由于其没有重写equals方法,因此直接调用从父类继承过来的equals方法,在比较过程中将忽略颜色信息 ,这样做虽然没有违反equals约定,但是不符合“逻辑相等”的期望,因此为ColorPoint提供equals方法:
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint)) {
return false;
}
return super.equals(o) && ((ColorPoint) o).color == color;
}
测试:
public static void main(String[] args) {
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
System.out.println(p.equals(cp));
System.out.println(cp.equals(p));
}
当p.equals(cp)时,使用的是Point中的equals方法,没有颜色信息因此返回true;当cp.equals(p)时,使用ColorPoint的equals方法,返回false,不满足对称性,可以通过重构equals的方法修复这个问题:
@Override
public boolean equals(Object o) {
if (!(o instanceof Point)) {
return false;
}
// 不带颜色的Point,使用Point的equals方法比较
if (!(o instanceof ColorPoint)) {
return o.equals(this);
}
return super.equals(o) && ((ColorPoint) o).color == color;
}
上述方法终于保证了对称性,我们可以使用以下测试数据来进行测试:
public static void main(String[] args) {
ColorPoint p1 = new ColorPoint(1, 2, Color.BLUE);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.RED);
System.out.println(p1.equals(p2));
System.out.println(p2.equals(p3));
System.out.println(p1.equals(p3));
}
当p1.equals(p2)时返回true;p2.equals(p3)时返回true;p1.equals(p3)时返回false,不满足传递性。
这个问题是面向对象语言中关于等价关系的一个基本问题: 无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals的约定。
解决这个问题的方法: 面向对象编程中,组合优先于继承 ,现在的ColorPoint类不再继承Point类,而是通过一个引用组合它,重构之后的代码如下:
public class ColorPoint {
private final Point point;
private final Color color;
public ColorPoint(int x, int y, Color color) {
if (color == null) {
throw new NullPointerException();
}
this.point = new Point(x, y);
this.color = color;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint)) {
return false;
}
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
}
}
java 类库中 java.sql.Timestamp 的 java.util.Date 就违反对称性约定,可以查看文档中的免责声明,这种做法不建议仿效。
非空性测试
很多类使用一个显式的null测试来避免抛出空指针异常:
@Override
public boolean equals(Object o) {
if (o == null) {
return false;
}
//...
return false;
}
其实在equals方法中,最终是将待比较对象转换为当前类的实例,以调用方法或访问属性, 这样必须先经过 instanceof ,而如果 instanceof 的第一个参数为null,则不管第二个参数是那种类型都会返回false,这样可以很好地避免空指针异常并且不需要单独地进行null测试。
@Override
public boolean equals(Object o) {
if (!(o instanceof MyType)) {
return false;
}
MyType mt = (MyType) o;
}
实现高质量equals方法的诀窍
1、使用==操作符检查 参数是否为这个对象的引用。
2、使用 instanceof 操作符检查 参数是否为正确的类型。
3、把参数转换成正确的类型。
4、对于要比较类中的每个关键域,检查参数中的域是否与该对象中对应的域相匹配。
5、编写完equals方法后需要测试是否满足对称性、传递性和一致性。
本条目最后的告诫
1、重写equals时总要重写hashCode
2、不要企图让equals方法过于智能
3、不要将equals声明中的Object对象替换为其他的类型,因为替换后只是重载Object.equals(Object o),而不是重写。