【问题标题】:Understanding equals method理解equals方法
【发布时间】:2015-09-09 00:07:48
【问题描述】:

J。 Bloch 在其有效的 Java 中为 equals 方法的实现提供了几个规则。他们在这里:

• 自反:对于任何非空引用值 x,x.equals(x) 必须 返回真。

• 对称:对于任何非空引用值 x 和 y, x.equals(y) 必须返回 true 当且仅当 y.equals(x) 返回 true。

• 传递:对于任何非空引用值 x、y、z,如果 x.equals(y) 返回 true 并且 y.equals(z) 返回 true,然后 x.equals(z) 必须返回 true。

• 一致:对于任何非空引用 值 x 和 y,x.equals(y) 的多次调用一致 如果未使用任何信息,则返回 true 或始终返回 false in equals 对对象的比较进行了修改。

• 对于任何非空 参考值 x, x.equals(null) 必须返回 false。

但后来他在书中提到了所谓的里氏替换原则:

Liskov 替换原则说,任何重要的属性 一个类型也应该适用于它的子类型,这样任何编写的方法 因为该类型应该同样适用于其子类型

我看不出它与equals 合同有何联系。在编写 equals 实现时,我们真的应该遵守它吗?

问题是关于实现子类的方法。这是书中的例子:

private static final Set<Point> unitCircle;

static {
    unitCircle = new HashSet<Point>();
    unitCircle.add(new Point(1, 0));
    unitCircle.add(new Point(0, 1));
    unitCircle.add(new Point(-1, 0));
    unitCircle.add(new Point(0, -1));
}

public static boolean onUnitCircle(Point p) {
    return unitCircle.contains(p);
}

public class CounterPoint extends Point {
    private static final AtomicInteger counter = new AtomicInteger();

    public CounterPoint(int x, int y) {
        super(x, y);
        counter.incrementAndGet();
    }

    public int numberCreated() { return counter.get(); }
}

以及以下实现:

// Broken - violates Liskov substitution principle (page 40)
@Override public boolean equals(Object o) {
    if (o == null || o.getClass() != getClass())
        return false;
    Point p = (Point) o;
    return p.x == x && p.y == y;
}

好的,违反了,然后呢?我不明白。

【问题讨论】:

  • 超类也不应该知道他的孩子,但对象知道字符串类^^ java 根本设计得不好。
  • @Zelldon 请详细说明,“对象知道字符串”是什么意思? (我没有遵循你的说法)
  • 对象有toString()方法。
  • 我实际上不明白你的问题中需要“但是”。矛盾在哪里?
  • 这是一个discussion,它显示了两难境地。

标签: java equals


【解决方案1】:

通常有两种方法可以在equals方法中检查类型:

选项 1:instanceof

if (! (obj instanceof ThisClass)){
    return false;
}

此选项尊重Liskov 替换原则。但是您不能在子类中添加与 equals 方法相关的其他属性,而不会破坏等价关系的特征(自反、对称、传递)。

选项 2:getClass()

if (obj == null || ! this.getClass().equals(obj.getClass())) {
    return false;
}

此选项违反Liskov 替换原则。但是您可以在子类中添加与 equals 方法相关的附加属性,而不会破坏等价关系的特征(自反、对称、传递)。

Joshua Bloch 在他的《Effective Java》一书中对此提出了警告。

Angelika Langer 提到了一种“混合类型”比较的方法,如果您可以为其他属性定义默认值:

http://www.angelikalanger.com/Articles/JavaSolutions/SecretsOfEquals/Equals-2.html

缺点是equals方法变得相当复杂。

// Broken - violates Liskov substitution principle (page 40)
@Override public boolean equals(Object o) {
    if (o == null || o.getClass() != getClass())
        return false;
    Point p = (Point) o;
    return p.x == x && p.y == y;
}

好的,违反了,然后呢?我不明白。

因此,如果您有一个子类,例如 MyPoint(可能会添加其他方法,但不会添加其他属性/字段),那么

Point p1 = new Point(x, y);
Point p2 = new MyPoint(x, y);

p1.equals(p2) == false

Set<Point> points = new HashSet<>();
points.add(p1);

points.contains(p2) == false;

虽然这两个对象确实代表同一个点。

如果您改用选项 1 (instanceof),equals 方法将返回 true。

【讨论】:

  • 问题是 J.B. 建议永远不要使用继承。他建议在这种情况下使用组合来代替。这是明确的概念,是的。他说我们永远无法以正确的方式为子类实现 equals,这让我感到很奇怪。
  • 抽象类的子类当然不考虑。
  • 我没有看到,在选项 1 中,为什么不能在子类中添加新属性。我的意思是,对于超类等于你不会只检查子类属性,只检查它自己的属性,子类应该检查它们自己的属性。如果您将一个对象与子类的实例进行比较,那么只有更高的类属性是相关的。
  • @St.Antario 我现在手头没有这本书,但我很确定这仅适用于具有与 equals 方法相关的附加字段/属性的子类(所以你必须重写equals方法->打破等价关系)。
  • @St.Antario 许多人使用选项 2 并承担后果。但一般来说,是的,我建议使用选项 1,而不是覆盖除 Object 之外的 equals 方法。您可以将其声明为 final 以确保它不会在某处被覆盖。
【解决方案2】:

我想他是想说一个点的特征是它的坐标。所以你会认为这是真的:

new Point(0, 0).equals(new CounterPoint(0, 0));

因为这两个点的坐标相同,即使它们的类型不同。但是建议的 equals 方法会返回 false,因为这两个对象有不同的类。

如果您以集合为例,这是正确的:

new LinkedList().equals(new ArrayList());

这两个列表的类型不同,但它们的内容相同(在这种情况下,它们都是空的),因此被认为是相等的。

【讨论】:

  • 因此,实现实际上并没有违反 equalses 合同,但我同意,这似乎违反直觉。这就是他建议我们不要以这种方式写 quals 的原因吗?