21

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

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

class Bar extends Foo {
    @Override
    void foo(Object foo) { ... }
}

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

我的问题是:为什么子类型(Bar)中的参数不能是超类型(Foo)中参数的超类型。在示例中,Object 是 String 的超类型。据我所知,允许这样做不会违反Liskov Substitution Principle

是否存在允许这样做会破坏代码的情况,还是当前 JLS 的限制?

4

7 回答 7

11

假设你能做到这一点。现在你的超类看起来像这样:

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

现在你的子类:

class Bar extends Foo {
    @Override
    void foo(Object foo) { ... }
}

该语言可能允许这样的事情(并且只需将 Foo.foo(String) 和 Foo.foo(Number) 分派给 Bar.foo(Object)),但显然这里 Java 的设计决定是一种方法只能覆盖正是另一种方法。

[编辑]

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

于 2013-07-05T10:23:52.030 回答
8

(从不同的角度重写......我原来的答案包含一个错误。:()

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

我相信从技术上讲它可以,并且不会破坏类型替换(Liskov Substitution Principle)之后的祖先合同。

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

从我下面的分析中,我推测/猜测不允许您的情况的理由:

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

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

编译器需要根据您的经验行事。 8.4 方法声明

子类中的方法可以覆盖祖先类中的方法当且仅当:

  • 方法名称相同(8.4.28.4.8.1
  • 在擦除泛型类型参数(8.4.28.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,它不包含可以移动到编译时的处理步骤,而不强制运行时类型与编译时类型完全匹配。不仅如此,它还对方法签名进行了简单的精确匹配,而不是复杂的“祖先类型匹配”

于 2013-07-08T05:19:25.327 回答
5

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

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

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

如果允许 Javafoo(Object)覆盖foo(String)该语言,则必须引入一种替代语法来指示重载方法的意图。例如,他们可以以类似于 C#newoverride方法声明的方式完成它。然而,无论出于何种原因,设计者都决定不采用这种新语法,而将语言保留在 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) { ... }
}

这是关于 ideone 的演示

于 2013-07-08T14:52:27.090 回答
2

答案很简单,在 Java 中,对于方法覆盖,您必须具有超类型的确切签名。但是,如果您删除@Override 注释,您的方法将被重载并且您的代码不会中断。这是一个 Java 实现,可确保您的意思是方法实现应该覆盖超类型的实现。

方法覆盖按以下方式工作。

class Foo{ //Super Class

  void foo(String string){

    // Your implementation here
  }
}

class Bar extends Foo{

  @Override
  void foo(String string){
    super(); //This method is implied when not explicitly stated in the method but the @Override annotation is present.
    // Your implementation here
  }

  // An overloaded method
  void foo(Object object){
    // Your implementation here
  }
}

上面显示的方法都是正确的,它们的实现可能会有所不同。

我希望这可以帮助你。

于 2013-07-11T00:09:51.200 回答
1

要回答帖子标题中的问题What is the reasoning behind not allowing supertypes on Java method overrides?

Java 的设计者想要一种简单的面向对象语言,他们特别拒绝了 C++ 的特性,在他们看来,该特性的复杂性/缺陷不值得受益。您所描述的可能属于设计师选择设计/指定功能的这一类。

于 2013-07-09T07:11:08.907 回答
0

你的代码的问题是你告诉编译器类中的foo方法Bar被 Bar 的父类覆盖,即 Foo。因为overridden methods must have same signature,但在您的情况下,根据语法,它是一个重载方法,因为您更改了 Bar 类的 foo 方法中的参数。

于 2013-07-09T06:12:41.420 回答
0

如果你可以用超类覆盖,为什么不也可以用子类呢?

考虑以下:

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

class Bar extends Foo {
  @Override
  void foo(Object foo) { ... }
}
class Another extends Bar {
  @Override
  void foo(Number foo) { ... }
}

现在您已经成功地覆盖了一个原始参数为 a 的方法String来接受 a Number。至少可以说是不可取的……

相反,可以通过使用重载和以下更明确的代码来复制预期的结果:

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

class Bar extends Foo {
    @Override
    private void foo(String foo) { ... }
    void foo(Object foo) { ... }
}
class Another extends Bar {
    @Override
    private void foo(Object foo) { ... }
    void foo(Number foo) { ... }
}
于 2013-07-10T15:18:30.837 回答