本文译自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)。