13

阅读:有效的 Java - Joshua Bloch 的第二版

第 8 项 - 在覆盖 equals 状态时遵守一般合同:

程序员编写一个看起来像这样的 equals 方法并花费数小时思考为什么它不能正常工作的情况并不少见:

[此处的代码示例]

问题是这个方法没有覆盖 Object.equals,它的参数是 Object 类型,而是重载了它。

代码示例:

public boolean equals(MyClass o) {
    //...
}

我的问题:

为什么像此代码示例中那样重载的强类型 equals 方法还不够?这本书指出重载而不是覆盖是不好的,但它没有说明为什么会这样,或者什么场景会使这个等于方法失败。

4

3 回答 3

20

这是因为重载该方法不会改变诸如集合或其他equals(Object)显式使用该方法的地方的行为。例如,采用以下代码:

public class MyClass {

    public boolean equals(MyClass m) {
        return true;
    }
}

如果你把它放在类似的东西中HashSet

public static void main(String[] args) {
    Set<MyClass> myClasses = new HashSet<>();
    myClasses.add(new MyClass());
    myClasses.add(new MyClass());
    System.out.println(myClasses.size());
}

这将打印2, not 1,即使您希望所有MyClass实例从您的重载中都是相等的,并且该集合不会添加第二个实例。

所以基本上,即使这是true

MyClass myClass = new MyClass();
new MyClass().equals(myClass);

这是false

Object o = new MyClass();
new MyClass().equals(o);

后者是集合和其他类用来确定相等性的版本。事实上,这将返回的唯一true地方是参数明确是其子类型的实例MyClass或其子类型之一。


编辑:根据您的问题:

覆盖与重载

让我们从覆盖和重载之间的区别开始。通过覆盖,您实际上重新定义了方法。您删除了它的原始实现并用您自己的实际替换它。所以当你这样做时:

@Override
public boolean equals(Object o) { ... }

您实际上是在重新链接您的新equals实现以替换来自Object(或任何最后定义它的超类)的实现。

另一方面,当你这样做时:

public boolean equals(MyClass m) { ... }

您正在定义一个全新的方法,因为您正在定义一个名称相同但参数不同的方法。HashSet调用时equals,它会在以下类型的变量上调用它Object

Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

(该代码来自 的源代码HashMap.put,用作 的底层实现HashSet.add。)

需要明确的是,它使用不同的唯一时间equalsequals方法被覆盖,而不是重载。如果你尝试添加@Override到你的重载equals方法,它会因为编译器错误而失败,并抱怨它没有覆盖一个方法。我什至可以equals在同一个类中声明这两种方法,因为它重载:

public class MyClass {

    @Override
    public boolean equals(Object o) {
        return false;
    }

    public boolean equals(MyClass m) {
        return true;
    }
}

泛型

至于泛型,不是equals泛型。它明确地作为它的类型,所以这一点没有实际意义。现在,假设您尝试这样做:Object

public class MyGenericClass<T> {

    public boolean equals(T t) {
        return false;
    }
}

这不会与消息一起编译:

名称冲突:MyGenericClass 类型的方法 equals(T) 与 Object 类型的 equals(Object) 具有相同的擦除,但不会覆盖它

如果您尝试这样@Override做:

public class MyGenericClass<T> {

    @Override
    public boolean equals(T t) {
        return false;
    }
}

你会得到这个:

MyGenericClass 类型的方法 equals(T) 必须覆盖或实现超类型方法

所以你赢不了。这里发生的是 Java 使用擦除实现泛型。当 Java 在编译时检查完所有泛型类型时,实际的运行时对象都被替换为Object. 你所看到T的任何地方,都包含实际的字节码Object。这就是为什么反射不适用于泛型类以及为什么你不能做类似list instanceof List<String>.

这也使得您不能重载泛型类型。如果你有这个课程:

public class Example<T> {
    public void add(Object o) { ... }
    public void add(T t) { ... }
}

您将从该add(T)方法中获得编译器错误,因为当类实际完成编译时,这些方法将具有相同的签名,public void add(Object).

于 2012-10-08T19:20:22.867 回答
12

为什么像此代码示例中那样重载的强类型 equals 方法还不够?

因为它不会覆盖Object.equals. Object任何只知道在中声明的方法(例如,测试键相等)的通用代码HashMap最终都不会调用您的重载——它们最终只会调用给出引用相等的原始实现。

请记住,重载是在编译时确定的,而覆盖是在执行时确定的。

如果您要覆盖equals,最好也提供一个强类型版本并从equals.

这是一个如何出错的完整示例:

import java.util.*;

final class BadKey {    
    private final String name;

    public BadKey(String name) {
        // TODO: Non-nullity validation
        this.name = name;
    }

    @Override
    public int hashCode() {
        return name.hashCode();
    }

    public boolean equals(BadKey other) {
        return other != null && other.name.equals(name);
    }
}

public class Test {

    public static void main(String[] args) throws Exception {
        BadKey key1 = new BadKey("foo");
        BadKey key2 = new BadKey("foo");
        System.out.println(key1.equals(key2)); // true

        Map<BadKey, String> map = new HashMap<BadKey, String>();
        map.put(key1, "bar");
        System.out.println(map.get(key2)); // null
    }
}

修复只是添加一个覆盖,如下所示:

@Override
public boolean equals(Object other) {
    // Delegate to the more strongly-typed implementation
    // where appropriate.
    return other instanceof BadKey && equals((BadKey) other);
}
于 2012-10-08T19:14:01.703 回答
2

因为使用 equals 的集合将使用Object.equals(Object)方法(可能在 MyClass 中被覆盖,因此被多态调用),这与MyClass.equals(MyClass).

重载一个方法定义了一个新的、不同的方法,它恰好与另一个方法同名。

于 2012-10-08T19:15:16.220 回答