阅读:有效的Java - Joshua Bloch的第二版
第8项 - 在覆盖等于州时遵守一般合同:
程序员编写一个equals方法并不罕见 看起来像这样,然后花费数小时困惑为什么它没有 工作正常:
[此处代码示例]
问题是这个方法不会覆盖Object.equals,它的 参数的类型为Object,但会重载它。
代码示例:
public boolean equals(MyClass o) {
//...
}
我的问题:
为什么强类型的equals方法像这个代码示例中的那样重载不够?该书指出,重载而不是覆盖是不好的,但它并没有说明为什么会出现这种情况或者什么情况会使这种等于方法失败。
答案 0 :(得分:17)
这是因为重载方法不会改变集合或其他显式使用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
个实例与您的重载相等,并且该集合也不会添加第二个实例。< / p>
所以基本上,即使这是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
的底层实现。)
要明确的是,唯一一次使用不同的equals
是equals
方法重写,而不是重载。如果您尝试将@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类型的等于(T)的方法与Object类型的equals(Object)具有相同的擦除但不覆盖它
如果您尝试@Override
:
public class MyGenericClass<T> {
@Override
public boolean equals(T t) {
return false;
}
}
你会得到这个:
MyGenericClass类型的等于(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)
。
答案 1 :(得分: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);
}
答案 2 :(得分:2)
因为使用equals的集合将使用Object.equals(Object)
方法(可能在MyClass中被覆盖,因此被称为多态),这与MyClass.equals(MyClass)
不同。
重载方法定义了一个新的,不同的方法,该方法恰好与另一个方法具有相同的名称。