继承关系下的equals改写


本作品采用知识共享署名-非商业性使用-相同方式共享 2.5 中国大陆许可协议 进行许可。

 

 

学习过Java的人都知道,Java对象的内容比较依靠的是Object类的equals方法。改写这个方法有严格的要求,JDK API中是这样描述的:


public boolean equals(Object obj);

指示其他某个对象是否与此对象“相等”。


equals 方法在非空对象引用上实现相等关系:

  • 自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true。
  • 对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 也一定返回 true。
  • 传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 也一定返回 true。
  • 一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改。
  • 对于任何非空引用值 x,x.equals(null) 一定返回 false。

注意:当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。

Josh Bloch在Effective Java一书中介绍了一种通用的改写equals的方法,Apache Commons Lang库也为我们提供了两个工具类来简化改写equals与hashCode的过程。关于这一点请参考利用 Commons Lang库改写equals与hashCode方法 一文。

具有继承关系的对象比较

五大特性中的对称性与传递性是其中比较麻烦的两个,特别是当我们要进行子类对象与父类对象的内容比较时。如下面的例子:
父类代码(Point类)
import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; public class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } @Override public boolean equals(Object other) { if (other == this) { return true; } if (!(other instanceof Point)) { return false; } Point that = (Point) other; return new EqualsBuilder() .append(this.getX(), that.getX()) .append(this.getY(), that.getY()) .isEquals(); } @Override public int hashCode() { return new HashCodeBuilder(17, 37) .append(this.getX()) .append(this.getY()) .toHashCode(); } }

子类代码(ColorPoint类)
import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; public class ColorPoint extends Point { private final Color color; public ColorPoint(int x, int y, Color color) { super(x, y); this.color = color; } public Color getColor() { return color; } @Override public boolean equals(Object other) { if (other == this) { return true; } if (!(other instanceof ColorPoint)) { return false; } ColorPoint that = (ColorPoint) other; return new EqualsBuilder() .appendSuper(super.equals(that)) .append(this.getColor(), that.getColor()) .isEquals(); } }

枚举型Color
public enum Color { RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET; }

当你想要对Point与ColoredPoint两种对象进行内容比较时,上述代码将会违反对称性原则。测试代码如下:
public static void main(String[] args) { Point p1 = new Point(1, 2); ColorPoint p2 = new ColorPoint(1, 2, Color.RED); System.out.println(p1.equals(p2)); // prints true System.out.println(p2.equals(p1)); // prints false }

那么,如何才能正确比较两个具有继承关系的对象呢?

Josh Bloch的解决方案

 

Josh Bloch在Effective Java一书中提供的解决方案是:复合优先于继承。也就是避开了继承问题,使ColorPoint与Point之间没有继承关系,进而完全不可比。ColorPoint类的改造如下:
import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; public class ColorPoint{ private final Point point; private final Color color; public ColorPoint(int x, int y, Color color) { this.point = new Point(x, y); this.color = color; } public Point getPoint() { return point; } public Color getColor() { return color; } @Override public boolean equals(Object other) { if (other == this) { return true; } if (!(other instanceof ColorPoint)) { return false; } ColorPoint that = (ColorPoint) other; return new EqualsBuilder() .append(this.getPoint(), that.getPoint()) .append(this.getColor(), that.getColor()) .isEquals(); } @Override public int hashCode() { return new HashCodeBuilder(17, 37) .append(this.getPoint()) .append(this.getColor()) .toHashCode(); } }

两种对象的比较结果均为false,测试代码如下:
public static void main(String[] args) { Point p1 = new Point(1, 2); ColorPoint p2 = new ColorPoint(1, 2, Color.RED); System.out.println(p1.equals(p2)); // prints false System.out.println(p2.equals(p1)); // prints false }

Josh Bloch为我们提供了避免错误的方法,但没有解决我们提出的问题!

Martin Odersky的解决方案

与Josh Bloch的解决方案相对应,Martin Odersky、Lex Spoon与Bill Venners三人在合作完成的How to Write an Equality Method in Java 一文中提出了另一个解决方案:canEqual 方法。此文的中文版可查看酷壳翻译整理的如何在Java中避免equals方法的隐藏陷阱 。本质上这个方案提供一个判断函数,判断哪些对象可以与本类对象进行比较。
据此,Point类的实现如下:
import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; public class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } @Override public boolean equals(Object other) { if (other == this) { return true; } // 此步骤判断“我”是否可以比较“他” if (!this.canEqual(other)) { return false; } Point that = (Point) other; // 此步骤判断“他”是否可以比较“我” if (!that.canEqual(this)) { return false; } return new EqualsBuilder() .append(this.getX(), that.getX()) .append(this.getY(), that.getY()) .isEquals(); } @Override public int hashCode() { return new HashCodeBuilder(17, 37) .append(this.getX()) .append(this.getY()) .toHashCode(); } public boolean canEqual(Object other) { // Point对象可以与所有继承自Point的对象进行比较 return (other instanceof Point); } }

ColorPoint类的实现如下:
import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; public class ColorPoint extends Point { private final Color color; public ColorPoint(int x, int y, Color color) { super(x, y); this.color = color; } public Color getColor() { return color; } @Override public boolean equals(Object other) { if (other == this) { return true; } // 此步骤判断“我”是否可以比较“他” if (!this.canEqual(other)) { return false; } ColorPoint that = (ColorPoint) other; // 此步骤判断“他”是否可以比较“我” if (!that.canEqual(this)) { return false; } return new EqualsBuilder() .appendSuper(super.equals(that)) .append(this.getColor(), that.getColor()) .isEquals(); } @Override public boolean canEqual(Object other) { // ColorPoint对象可以与所有继承自ColorPoint的对象进行比较 return (other instanceof ColorPoint); } }

测试过程如下:
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)); // prints false System.out.println(cp.equals(p)); // prints false }

Martin的方案很好的解释了为什么ColorPoint对象不能与Point对象进行比较,但这似乎并不能解决如何才能让ColorPoint对象与Point对象进行比较的问题。利用canEqual是否可以完成这一目的呢?

补充说明

在这里提供另一个类ColorPointEx,实现了可以与所有继承自Point的对象进行比较的功能。并且比较是有选择的:如果比较的对象也是继承自ColorPointEx,则比较的内容包括Color信息;如果比较的对象是继承自 Point,则比较的内容不包括Color信息。ColorPointEx的代码实现如下:
import org.apache.commons.lang.builder.EqualsBuilder; public class ColorPointEx extends Point { private final Color color; public ColorPointEx(int x, int y, Color color) { super(x, y); this.color = color; } public Color getColor() { return color; } @Override public boolean equals(Object other) { if (other == this) { return true; } if (!this.canEqual(other)) { return false; } if (other instanceof ColorPointEx) { // ColorPoint对象之间的比较 ColorPointEx that = (ColorPointEx) other; if (!that.canEqual(this)) { return false; } return new EqualsBuilder() .appendSuper(super.equals(that)) .append(this.getColor(), that.getColor()) .isEquals(); } if (other instanceof Point) { // Point对象之间的比较 Point that = (Point) other; if (!that.canEqual(this)) { return false; } return new EqualsBuilder() .appendSuper(super.equals(that)) .isEquals(); } return false; } @Override public boolean canEqual(Object other) { return (other instanceof ColorPointEx || other instanceof Point ); } }

测试代码如下:
public static void main(String[] args) { Point p = new Point(1, 2); ColorPoint cp = new ColorPoint(1, 2, Color.RED); ColorPointEx cpe = new ColorPointEx(1, 2, Color.BLUE); ColorPointEx cpe2 = new ColorPointEx(1, 2, Color.RED); System.out.println(p.equals(cp)); // prints false System.out.println(cp.equals(p)); // prints false System.out.println(p.equals(cpe)); // prints true System.out.println(cpe.equals(p)); // prints true System.out.println(cp.equals(cpe)); // prints false System.out.println(cpe.equals(cp)); // prints false System.out.println(cpe.equals(cpe2)); // prints false System.out.println(cpe2.equals(cpe)); // prints false }

可以说,Martin的canEqual方案可以解决继承关系下的对象比较问题,但前提是你必须正确的使用它。

你可能感兴趣的:(java,String,object,equals,Class,import)