Object.equals方法:重载还是覆盖

本文译自StackOverflow上对此问题的讨论。

原问题链接

 

在阅读Joshua Bloch的《Effective Java(第二版)》第8条“覆盖equals时请遵守通用约定”时对如下论述有疑问:

“不要将equals声明中的Object对象替换为其他的类型。程序员编写出下面这样的equals方法并不鲜见,这会使程序员花上数个小时都搞不清它为什么不能正常工作:”

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

 “问题在于,这个方法并没有覆盖(override)Object.equals,因为它的参数应当是Object类型,相反,它重载(overload)了Object.equals。”

 

问题:

为何代码示例中的强类型的equals方法重载并不足够?书中提到重载而非覆盖会引起问题,但并未论述为何如此也没有说明在何种场景下会使得equals方法失败。

 

回答:

这是因为重载此方法并不会改变集合类或者其他地方显式调用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,而不是1。虽然你期望所有的MyClass实例经由重载方法判断都是相等,并且集合不会添加第二个实例。

所以基本上,即使下面表达式为true:

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

 下述表达式依然为false:

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

后一个表达式是集合或其他类用于判断相等性的。事实上,只有当参数显式地为MyClass或其子类型的实例时,才会调用到重载方法并返回true。

 

关于覆盖还是重载的问题:

让我们从覆盖和重载的区别说起。通过覆盖,你事实上重新定义了这个方法。事实上相当于你删除了方法原始的实现并替换为自己的实现。所以当你这样做时:

@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的底层实现。)

为了更清楚,MyClass中的equals方法只有当被覆盖时才会被调用,而不是被重载的时候。如果你试图在一个重载的equals方法上添加@Override注解,它将会产生一个编译错误,指出它并没有覆盖一个方法。我们可以在一个类中声明两个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)相同,但并未覆盖它

Name clash: The method equals(T) of type MyGenericClass has the same erasure as equals(Object) of type Object but does not override it

 当添加@Override注解时:

public class MyGenericClass<T> {

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

 错误信息变为:MyGenericClass的equals(T)方法必须覆盖或实现父类方法

The method equals(T) of type MyGenericClass must override or implement a supertype method

于是怎么做都会有问题。原因在于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)。

你可能感兴趣的:(java,generics,override,overload)