不允许Java方法覆盖的超类型的原因是什么?

Gre*_*her 20 java overloading

编译器认为以下代码无效:

class Foo {
    void foo(String foo) { ... }
}

class Bar extends Foo {
    @Override
    void foo(Object foo) { ... }
}
Run Code Online (Sandbox Code Playgroud)

我认为这在JLS 8.4.8.1中有描述:"m1的签名是m2签名的子签名(§8.4.2)." 在8.4.2中:"相应类型变量的边界是相同的".

我的问题是:为什么子类型(Bar)中的参数不能成为超类型(Foo)中参数的超类型.在示例中,Object是String的超类型.据我所知,这不会违反Liskov替代原则.

是否有允许这会破坏代码的情况或者它是当前JLS的限制?

kut*_*kem 11

假设你可以那样做.现在你的超类看起来像这样:

class Foo {
    void foo(String foo) { ... }
    void foo(Number foo) { ... }
}
Run Code Online (Sandbox Code Playgroud)

和你的子类现在:

class Bar extends Foo {
    @Override
    void foo(Object foo) { ... }
}
Run Code Online (Sandbox Code Playgroud)

语言可能会允许这样的事情(和公正派遣两个Foo.foo(字符串)和Foo.foo(号码),以Bar.foo(对象)),但显然Java的设计决定在这里是一个方法只能覆盖正是另一种方法.

[编辑]

正如dasblinkenlight在他的回答中所说,没有@Override可以有一个foo(Object),但这只会重载foo函数,并且不会覆盖它们.在调用时,java选择最具体的方法,因此总是将foo("Hello World")分派到foo(String)方法.


Gle*_*est 6

(重写,从不同的角度......我的原始答案包含错误.:()

为什么子类型(Bar)中的参数不能成为超类型(Foo)中参数的超类型.

我认为技术上它可以,并且它不会破坏类型替代后的祖先合同(利斯科夫替代原则).

  • 类型是按值传递的(包括引用类型).
  • 永远不会强制调用者处理与传入的参数类型不同的参数类型.
  • 方法体可以交换参数类型,但不能将其返回给调用者(不是"输出参数类型").
  • 如果您的提议被允许,并且对祖先方法签名进行了调用,则后代类可以使用更广泛的类型覆盖该方法,但仍然无法返回比调用者设置的更广泛的类型.
  • 覆盖永远不会破坏使用窄祖先方法契约的客户端.

根据我在下面的分析,我推测/猜测不允许你的场景的理由:

  • 与性能相关:允许更广泛的覆盖类型会影响运行时性能,这是一个主要问题
  • 与功能相关:它只增加了少量功能.就目前而言,您可以将"更广泛的方法"添加为重载方法而不会覆盖.然后,您可以使用精确的签名匹配覆盖原始方法.最终结果:你在功能上实现了非常相似的东西.

方法覆盖的编译器要求 - JLS 7

编译器需要根据您的经验采取行动. 8.4方法声明:

子类中的方法可以覆盖祖先类iff中的方法:

  • 方法名称相同(8.4.2和8.4.8.1)
  • 擦除泛型类型参数后,方法参数具有相同的类型(8.4.2和8.4.8.1)
  • 返回类型是祖先类中返回类型的类型可替换,即相同类型或更窄(8.4.8.3)

    注:子签名并不能意味着压倒一切的方法使用重载方法的亚型.覆盖方法据说有一子签名重写方法的覆盖方法具有当完全一样,除了一般类型的类型签名和相应的原始类型被认为是等效的.


编译器v方法匹配和调用的运行时处理

通过多态类型匹配,性能匹配方法签名.通过将覆盖方法签名限制为祖先的精确匹配,JLS将大部分处理移动到编译时. 15.12方法调用表达式 - 总结:

  1. 确定要搜索的类或接口(编译时确定)

    • 获取调用该方法的基类型T.
    • 这是向编译器声明的引用类型,而不是运行时类型(可以替换子类型).
  2. 确定方法签名(编译时确定)

    • 搜索编译时基类型T,以获取与调用一致的名称和参数以及返回类型匹配的适用方法.
      • 解析T的泛型参数,从调用方法参数的类型显式传递或隐式推断
      • 阶段1:通过一致类型/子类型('子类型')适用的方法
      • 第2阶段:通过自动装箱/拆箱加上子类型适用的方法
      • 第3阶段:通过自动装箱/拆箱加上亚型加上变量'arity'参数的方法
      • 确定最具体的匹配方法签名(即可以成功传递给所有其他匹配方法签名的方法签名); 如果没有:编译器/模糊错误
  3. 检查:选择的方法是否合适?(编译时确定)

  4. 方法调用评估(运行时确定)

    • 确定运行时目标引用类型
    • 评估参数
    • 检查方法的可访问性
    • 定位方法 - 与编译时匹配的签名完全匹配
    • 调用

表现命中

根据JLS中的文字细分:

步骤1:5%步骤2:60%步骤3:5%步骤4:30%

第2步不仅在文本中体积庞大,而且令人惊讶地复杂.它具有复杂的条件和许多昂贵的测试条件/搜索.在此处最大化编译器执行更复杂和更慢处理是有利的.如果这是在运行时完成的,那么性能就会受到拖累,因为每次方法调用都会发生这种情况.

第4步仍然有重要的处理,但它尽可能简化.阅读15.12.4,它不包含可以移动到编译时的处理步骤,而不强制运行时类型与编译时类型完全匹配.不仅如此,它还对方法签名进行了简单的精确匹配,而不是复杂的"祖先类型匹配"


das*_*ght 5

Java 注释不能改变编译器从您的类生成字节码的方式。您使用注解来告诉编译器您认为您的程序应该如何被解释,并在编译器的解释与您的意图不符时报告错误。但是,您不能使用注释来强制编译器生成具有不同语义的代码。

当您的子类声明了一个与其超类具有相同名称和参数数量的方法时,Java 必须在两种可能性之间做出决定:

  • 您希望子类中的方法覆盖超类中的方法,或者
  • 您希望子类中的方法重载超类中的方法。

如果 Java 允许foo(Object)覆盖foo(String)该语言,则必须引入替代语法来指示重载方法的意图。例如,他们可以用类似于 C# 的方式new和override方法声明来完成它。然而,无论出于何种原因,设计人员决定反对这种新语法,让语言使用 JLS 8.4.8.1 中指定的规则。

请注意,当前设计允许您通过转发来自重载函数的调用来实现覆盖的功能。在你的情况下,这意味着Bar.foo(Object)从Foo.foo(String)这样调用:

class Foo {
    public void foo(String foo) { ... }
}

class Bar extends Foo {
    @Override
    public void foo(String foo) { this.foo((Object)foo); }
    public void foo(Object foo) { ... }
}
Run Code Online (Sandbox Code Playgroud)

这是ideone 上的演示。