ITEM 40: CONSISTENTLY USE THE OVERRIDE ANNOTATION
Java库包含几种注解类型。对于一个程序员来说,最重要的是@Override。此注解只能用于方法声明,它指示带注解的方法声明覆盖超类型中的声明。如果您始终使用这个注解,它将保护您免受大量恶意bug的攻击。考虑下面这个例子,其中类 Bigram 表示一个 双字母组,或有序的字母对:
// Can you spot the bug?
public class Bigram {
private final char first;
private final char second;
public Bigram(char first, char second) {
this.first = first;
this.second = second;
}
public boolean equals(Bigram b) {
return b.first == first && b.second == second;
}
public int hashCode() {
return 31 * first + second;
}
public static void main(String[] args) {
Set<Bigram> s = new HashSet<>();
for (int i = 0; i < 10; i++)
for (char ch = 'a'; ch <= 'z'; ch++)
s.add(new Bigram(ch, ch));
System.out.println(s.size());
}
}
主程序反复向一个集合添加26个bigram,每个bigram由两个相同的小写字母组成。然后它打印集合的大小。如果你试着运行这个程序,你会发现它输出的不是26而是260。有什么问题吗?
显然,Bigram 类的作者打算重写 equals 方法(iem 10),甚至还记得同时重写 hashCode (item 11)。不幸的是,程序员没有重写equals,而是重载了它(item 52)。为了覆盖 Object 的 equals 方法,我们必须定义一个参数是Object类型的equals方法,但是 Bigram 的 equals 方法的参数不是 Object 类型的,所以 Bigram 继承了 Object 的 equals 方法。这个 equals 方法测试对象标识,就像 == 操作符一样。每一个双字母的十份副本都与其他九份不同,因此它们被认为是不相等的。等于,这就解释了为什么程序输出260。
幸运的是,编译器可以帮助您找到这个错误,但前提是您要告诉它您打算覆盖 Object.equals。为此,请在 Bigram.equals 使用注解 @Override,如下所示:
@Override
public boolean equals(Bigram b) {
return b.first == first && b.second == second;
}
如果插入此注解并尝试重新编译程序,编译器将生成如下错误消息: "Bigram.java:10: method does not override or implement a method from a supertype"
你会立刻意识到自己做错了什么,给自己一记耳光,用正确的一记替换掉错误的 equals 实现(item 10):
@Override
public boolean equals(Object o) {
if (!(o instanceof Bigram))
return false;
Bigram b = (Bigram) o;
return b.first == first && b.second == second;
}
因此,您应该在您认为要覆盖超类声明的每个方法声明上使用 Override 注解。这条规则有一个小小的例外。如果您正在编写一个没有标记为abstract 的类,并且您认为它覆盖了其超类中的抽象方法,那么您不需要在该方法上添加 Override 注解。在没有声明抽象的类中,如果未能覆盖抽象超类方法,编译器将发出错误消息。
不过,您可能希望将注意力放在类中覆盖超类方法的所有方法上,在这种情况下,您也可以随意注解这些方法。大多数 IDE 都可以设置为在选择重写方法时自动插入覆盖注解。
大多数 IDE 都提供了一致使用覆盖注解的另一个原因:如果启用了适当的检查,如果您的方法没有覆盖注解,但是覆盖了超类方法,IDE 将生成一个警告。如果您始终如一地使用 Override 注解,这些警告将提醒您进行无意的覆盖。它们补充了编译器的错误消息,这些错误消息会警告您无意中覆盖失败。在 IDE 和编译器之间,您可以确保您要覆盖的方法精准的命中了,而不是在其他任何地方。
覆盖注解可以用于覆盖来自接口和类的声明的方法声明。随着缺省方法的出现,在接口方法的具体实现上使用覆盖来确保签名是正确的,这是一个很好的实践。如果知道接口没有默认方法,可以选择忽略接口方法的具体实现上的覆盖注解,以减少混乱。
但是,在抽象类或接口中,值得注解所有您认为覆盖超类或超接口方法的方法,无论是具体的还是抽象的。例如,Set 接口没有向 Collection 接口添加任何新方法,因此它应该包含覆盖其所有方法声明的注解,以确保不会意外地向集合接口添加任何新方法。
总之,如果您在每个方法声明上都使用 Override 注解,那么编译器可以保护您不受很多错误的影响,因为您认为每个方法声明都要覆盖超类型声明,只有一个例外:在具体类中,您不需要注解您认为可以覆盖抽象方法声明的方法(尽管这样做没有害处)。
网友评论