什么时候重写Equals方法
如果类具有自己特有的“逻辑相等”概念,而且父类还没有重写 equals 方法以实现期望的行为时,就需要重写 equals 方法。
这样做还可以使得这个类的实例可以被用作 Map 的 Key,或者 Set 的元素,使 Map 或 Set 表现出预期的行为来。
重写Equals的期望结果
在重写 equals 方法的时候,如果满足了以下任何一个条件,就正是所期望的结果:
- 类的每个实例本质上都是唯一的;
- 不关心类是否提供了 logical equality 的测试功能;
- 父类已经覆盖了 equals 方法,从父类继承过来的行为对于子类也是合适的;
- 类是私有的或者是包级私有的,确保它的 equals 方法永远不会被调用时需要重写 equals 方法;
重写Equals的原则
自反性(reflexive):对于任何非 null 的引用值 x,x.equals(x) 必须返回 true;
对称性(symmetric):对于任何非 null 的引用值 x and y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 必须返回 true;
传递性(consistent):对于任何非 null 的引用值 x y z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 也必须返回true;
一致性(consistent):对于任何非 null 的引用值 x and y,只要equals的比较操作在对象中所用的信息没有被修改,那么多次调用 x.equals(y) 都会一致地返回 true,或者一致地返回 false;
非空性(non nullity):对于任何非 null 的引用值 x,x.equals(null) 必须返回 false;
自反性
一般不会违背这个原则,如果违背了,那么出现的现象会是将该类的实例添加到集合后,调用 contains 方法查找这个实例,会得到一个 false。
对称性
对称性简单说是,任何两个对象对于它们是否相等的问题都必须保持一致, x 等于 y 那么 y 也要等于 x。举个违反这个原则的例子:
package test.ch01;
public 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;
}
}
在这个类中,equals 要做到与普通的字符串比较时不区分大小写,其问题在于 String 类中的 equals 方法并不知道不区分大小写,因此反过来比较并不成立,违反了对称性。
package test.ch01;
public class Test {
public static void main(String[] args) {
CaseInsensitiveString cis = new CaseInsensitiveString("Hello");
String s = "hello";
System.out.println(cis.equals(s)); // true
System.out.println(s.equals(cis)); // false
}
}
解决这个问题,只需要把企图与 String 互操作的代码从 equals 方法中去掉就可以了:
package test.ch01;
public 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) {
return o instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
}
package test.ch01;
public class Test {
public static void main(String[] args) {
CaseInsensitiveString cis = new CaseInsensitiveString("Hello");
String s = "hello";
CaseInsensitiveString cis1 = new CaseInsensitiveString("hello");
System.out.println(cis.equals(s)); // false
System.out.println(s.equals(cis)); // false
System.out.println(cis.equals(cis1)); // true
}
}
传递性
传递性要求 x 等于 y,y 等于 z,那么 x 也要等于 z。但是,此处有非常重要的一点,面向对象语言关于等价关系的一个基本问题:
无法在扩展可实例化的类的同时,即增加新的值组件,同时又保留 equals 约定,除非愿意放弃面向对象的抽象所带来的优势。
考虑两个类,它们是继承关系:
package test.ch01;
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;
}
}
package test.ch01;
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) {
if (!(o instanceof ColorPoint)) {
return false;
}
return super.equals(o) && ((ColorPoint) o).color == color;
}
}
如果 ColorPoint 类,不重写 equals,而是直接从 Point 继承过来,那么 ColorPoint 与 Point 比较时会忽略掉颜色。
但是如果按照上面代码,重写 equals,那么会违反一致性:
Point p1 = new Point(1, 2);
ColorPoint cp1 = new ColorPoint(1, 2, Color.RED);
System.out.println(p1.equals(cp1)); // true
System.out.println(cp1.equals(p1)); // false
总之面向对象语言的等价交换关系是一个问题。
里氏替换原则(Liskov substitution principle)认为,一个类型的任何重要属性也将适用于他的子类,因此为该类型编写的任何方法,在它的子类型上也应该同样运行的很好。
虽然没有一种令人满意的办法解决上面的问题,但是还是有一个不错的权宜之计:
package test.ch01;
public class ColorPoint {
private final Point point;
private final Color color;
public ColorPoint(Point point, Color color) {
this.point = point;
this.color = color;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Color)) {
return false;
}
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
}
}
使用复合方式代替继承,可以解决上面的问题。
一致性
可变对象在不同的时候可以与不同的对象相等,不可变的对象则不会这样。如果认为它应该是不可变的,就必须保证 equals 方法满足:相等的对象永远相等,不想等的对象永远不相等。
无论类是否是不可变的,都不要使 equals 方法依赖不可靠的资源。
非空性
所有对象都必须不等于 null。
高质量的 Equals 方法
1.使用 == 操作符检查“参数是否为这个对象的引用”,如果比较操作有可能很昂贵,就值得先这么做;
2.使用instanceof检查“参数类型是否正确”;
3.把参数转换成正确的类型,在instanceof后,所以会保证成功;
4.对每个关键域,检查参数中的域是否与该对象中对应的域相匹配,float 和 double需要用 Float.compare 和 Double.compare 比较,集合用 Arrays.equals 比较;
5.对于引用域可以为 null 的情况,为了避免导致 NPE,可以写成 :
(field == o.filed || (field != null && field.equals(o.field)))
6.最先比较最有可能不一致的域,或者是开销低的域,最好是同时满足这两个条件;
7.重写 equals 时总要重写 hashCode;
8.不要让 equals 方法过于智能;
9.不要将 equals 声明中的 Object 对象替换为其他类型;
10.最好用 @Override 注解描述 equals;