40. 始终使用 Override 注解

  Java 类库包含几个注解类型。对于典型的程序员来说,最重要的是 @Override。此注解只能在方法声明上使用,它表明带此注解的方法声明重写了父类的声明。如果始终使用这个注解,它将避免产生大量的恶意 bug。考虑这个程序,在这个程序中,类 Bigram 表示双字母组合,或者是有序的一对字母:

  1. // Can you spot the bug?
  2. public class Bigram {
  3. private final char first;
  4. private final char second;
  5. public Bigram(char first, char second) {
  6. this.first = first;
  7. this.second = second;
  8. }
  9. public boolean equals(Bigram b) {
  10. return b.first == first && b.second == second;
  11. }
  12. public int hashCode() {
  13. return 31 * first + second;
  14. }
  15. public static void main(String[] args) {
  16. Set<Bigram> s = new HashSet<>();
  17. for (int i = 0; i < 10; i++)
  18. for (char ch = 'a'; ch <= 'z'; ch++)
  19. s.add(new Bigram(ch, ch));
  20. System.out.println(s.size());
  21. }
  22. }

  主程序重复添加二十六个双字母组合到集合中,每个双字母组合由两个相同的小写字母组成。 然后它会打印集合的大小。 你可能希望程序打印 26,因为集合不能包含重复项。 如果你尝试运行程序,你会发现它打印的不是 26,而是 260。它有什么问题?

  显然,Bigram 类的作者打算重写 equals 方法(详见第 10 条),甚至记得重写 hashCode(详见第 11 条)。 不幸的是,我们倒霉的程序员没有重写 equals,而是重载它(详见第 52 条)。 要重写 Object.equals,必须定义一个 equals 方法,其参数的类型为 Object,但 Bigramequals 方法的参数不是 Object 类型的,因此 Bigram 继承 Objectequals 方法,这个 equals 方法测试对象的引用是否是同一个,就像 == 运算符一样。 每个字母组合的 10 个副本中的每一个都与其他 9 个副本不同,所以它们被 Object.equals 视为不相等,这就解释了程序打印 260 的原因。

  幸运的是,编译器可以帮助你找到这个错误,但只有当你通过告诉它你打算重写 Object.equals 来帮助你。 要做到这一点,用 @Override 注解 Bigram.equals 方法,如下所示:

  1. @Override public boolean equals(Bigram b) {
  2. return b.first == first && b.second == second;
  3. }

  如果插入此注解并尝试重新编译该程序,编译器将生成如下错误消息:

  1. Bigram.java:10: method does not override or implement a method
  2. from a supertype
  3. @Override public boolean equals(Bigram b) {
  4. ^

  你会立刻意识到你做错了什么,在额头上狠狠地打了一下,用一个正确的(详见第 10 条)来替换出错的 equals 实现:

  1. @Override public boolean equals(Object o) {
  2. if (!(o instanceof Bigram))
  3. return false;
  4. Bigram b = (Bigram) o;
  5. return b.first == first && b.second == second;
  6. }

  因此,应该在你认为要重写父类声明的每个方法声明上使用 Override 注解。 这条规则有一个小例外。 如果正在编写一个没有标记为抽象的类,并且确信它重写了其父类中的抽象方法,则无需将 Override 注解放在该方法上。 在没有声明为抽象的类中,如果无法重写抽象父类方法,编译器将发出错误消息。 但是,你可能希望关注类中所有重写父类方法的方法,在这种情况下,也应该随时注解这些方法。 大多数 IDE 可以设置为在选择重写方法时自动插入 Override 注解。

  大多数 IDE 提供了是种使用 Override 注解的另一个理由。 如果启用适当的检查功能,如果有一个方法没有 Override 注解但是重写父类方法,则 IDE 将生成一个警告。 如果始终使用 Override 注解,这些警告将提醒你无意识的重写。 它们补充了编译器的错误消息,这些消息会提醒你无意识重写失败。 IDE 和编译器,可以确保你在任何你想要的地方和其他地方重写方法,万无一失。

  Override 注解可用于重写来自接口和类的方法声明。 随着 default 默认方法的出现,在接口方法的具体实现上使用 Override 以确保签名是正确的是一个好习惯。 如果知道某个接口没有默认方法,可以选择忽略接口方法的具体实现上的 Override 注解以减少混乱。

  然而,在一个抽象类或接口中,值得标记的是你认为重写父类或父接口方法的所有方法,无论是具体的还是抽象的。 例如,Set 接口不会向 Collection 接口添加新方法,因此它应该在其所有方法声明中包含 Override 注解以确保它不会意外地向 Collection 接口添加任何新方法。

  总之,如果在每个方法声明中使用 Override 注解,并且认为要重写父类声明,那么编译器可以保护免受很多错误的影响,但有一个例外。 在具体的类中,不需要注解标记你确信可以重写抽象方法声明的方法(尽管这样做也没有坏处)。