重写 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方法中抛出异常。
@Overridepublic 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)); }输出:
truefalse当调用 cis.equals(str) 时,使用的是CaseInsensitiveString类的equals方法返回true;但是str.equals(cis)调用的是String类的equals方法返回false;String类是不知道cis是什么鬼,更不知道如何和cis进行比较,肯定返回false ,这就不满足对称性了。
解决这个问题的方法,把企图和String互操作的代码从equals中删掉就可以了。
@Overridepublic 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方法:
@Overridepublic 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的方法修复这个问题:
@Overridepublic 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测试来避免抛出空指针异常:
@Overridepublic boolean equals(Object o) { if (o == null) { return false; } //... return false;}其实在equals方法中,最终是将待比较对象转换为当前类的实例,以调用方法或访问属性, 这样必须先经过 instanceof ,而如果 instanceof 的第一个参数为null,则不管第二个参数是那种类型都会返回false,这样可以很好地避免空指针异常并且不需要单独地进行null测试。
@Overridepublic 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),而不是重写。
新闻热点
疑难解答