首页 > 学院 > 开发设计 > 正文

08-重写 equals 时请遵守通用约定

2019-11-08 03:13:02
字体:
来源:转载
供稿:网友

重写 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),而不是重写。


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表