覆盖Object.equals VS重载它
阅读:有效的Java – Joshua Bloch的第二版
第8项 – 在覆盖等于国家时遵守一般合同:
程序员编写一个看起来像这样的equals方法,然后花费数小时来解释为什么它不能正常工作的情况并不少见:
[此处代码示例]
问题是此方法不会覆盖Object.equals,其参数类型为Object,但会重载它。
代码示例:
public boolean equals(MyClass o) { //... }
我的问题:
为什么强类型的equals方法像这个代码示例中的那样重载不够? 该书指出,重载而不是覆盖是不好的,但它没有说明为什么会出现这种情况或者什么情况会使这种等于方法失败。
这是因为重载方法不会改变集合或其他显式使用equals(Object)
方法的地方的行为。 例如,请使用以下代码:
public class MyClass { public boolean equals(MyClass m) { return true; } }
如果你把它放在像HashSet
这样的东西:
public static void main(String[] args) { Set 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);
而后者是集合和其他类用于确定相等性的版本。 事实上, 唯一会返回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))) {
(该代码来自HashSet.add
的源代码,该代码用作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; } }
generics
至于generics, equals
不是通用的。 它明确地将Object
作为其类型,因此这一点没有实际意义。 现在,假设你试图这样做:
public class MyGenericClass { public boolean equals(T t) { return false; } }
这将无法使用以下消息进行编译:
名称冲突:MyGenericClass类型的方法equals(T)与Object类型的equals(Object)具有相同的擦除但不覆盖它
如果您尝试@Override
它:
public class MyGenericClass { @Override public boolean equals(T t) { return false; } }
你会得到这个:
方法等于MyGenericClass类型的(T)必须覆盖或实现超类型方法
所以你不能赢。 这里发生的是Java使用擦除来实现generics。 当Java在编译时完成所有generics类型的检查时,实际的运行时对象全部被Object
替换。 你看到T
到处都是,实际的字节码包含了Object
。 这就是为什么reflection不能很好地处理generics类以及为什么你不能做像list instanceof List
这样的事情。
这也使得您无法使用generics类型进行重载。 如果你有这个课程:
public class Example { public void add(Object o) { ... } public void add(T t) { ... } }
您将从add(T)
方法中获得编译器错误,因为当类实际完成编译时,这些方法将具有相同的签名, public void add(Object)
。
为什么强类型的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 map = new HashMap(); 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); }
因为使用equals的集合将使用Object.equals(Object)
方法(可能在MyClass中被覆盖,因此被称为多态),这与MyClass.equals(MyClass)
。
重载方法定义了一种新的,不同的方法,恰好与另一方法具有相同的名称。