因为在 java 中,覆盖不是可选的。
类级别的方法名称。
在类级别(例如,类文件中的内容以及 JVM 执行的内容),方法名称包括它们的返回类型和参数类型(当然还有名称)。在 JVM 级别,不存在 varargs(它是一个数组),不存在泛型(出于签名目的将它们删除),并且throws 子句不是故事的一部分。但除此之外,这个方法:
public void foo(String foo, int bar, boolean[] baz, long... args) throws Exception {}
在类文件级别变成这个名字:
foo(Ljava/lang/String;I[Z[J)V
这看起来像 gobbledygook,但[ 是“数组”,每个基元都有一个字母(Z 代表布尔值,J 代表长整数,I 代表整数),V 代表 void,L 代表:对象类型.现在说得通了。
这实际上是类级别的方法名称(嗯,我们称之为签名)。 ANY 调用 java 中的方法,在类级别,总是使用完整的签名。这意味着javac 根本无法编译方法调用,除非它实际上知道您正在调用的确切方法,这就是为什么javac 不起作用,除非您在编译时拥有您调用的所有内容的完整类路径。
覆盖不是可选的!
在类级别,如果您定义的方法的完整签名与父类中的签名完全匹配,则它会覆盖该方法。时期。你不能不。 @Override 作为注解丝毫不会影响这一点(如果您没有覆盖任何内容,该注解只会导致编译器抱怨,它是经过编译器检查的文档,仅此而已)。
javac 更进一步
作为一个语言的东西,javac 会在你想收紧返回类型时搭建桥梁。给定:
class Parent {
Object foo() { return null; }
}
class Child extends Parent {
String foo() { return null; }
}
然后在类级别,Parent 中的一个方法的完整签名是foo()Ljava/lang/Object;,而 Child 中的一个方法具有 foo()Ljava/lang/String;,因此这些方法不同,并且 Child 的 foo 似乎不会覆盖 Parent 的富。
但是javac 介入,并且确实使这些覆盖。它通过在 Child 中实际创建 2 个方法 来做到这一点。你可以看到这一点!编写上面的代码,编译它,然后在 Child 上运行 javap -c -v,你会看到这些。 javac 有 2 个方法:foo()Ljava/lang/String; 和 foo()Ljava/lang/Object;(它们确实具有相同的签名,因此根据定义覆盖了 Parent 的实现)。第二个实现为仅调用“真实”foo(返回字符串),并获得synthetic 标志。
决赛就是这样
这最终解决了你的问题:鉴于final 说:我不能被覆盖,那么,就是这样。您现在已经制定了 2 条互斥规则:
- 父母的 foo 不能被覆盖
- 根据定义(因为其签名匹配),孩子的 foo 会覆盖父母的 foo
Javac 将在此结束,在您的脸上抛出一个错误,然后收工。如果您想象一些假设的 javac 更新,这些因素的组合应该导致 javac 创建一个单独的方法:但是,如何?在类级别,相同的签名 == 相同的方法(这是一个覆盖),那么你建议什么?那个java在名字后面加了0?
如果是这样,javac应该如何处理:
Parent p = new Child();
p.foo();
那里是哪个foo? foo()Ljava/lang/Object; 来自父母,还是foo0()L/java/Object; 来自孩子?
您可以编写一个规范来回答这个问题(大概,这里很明显:Parent's foo;你是否写过Child c = new Child(); c.foo(); 然后foo0 是有意的,但这使得语言变得相当复杂,以及用于什么目的?
Java 语言设计者认为这不是一个有用的练习,因此没有将这种复杂性添加到语言中。我很确定这显然是正确的选择,但您的意见当然可能不同。