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

时间:2013-07-05 09:36:23

标签: java overloading

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

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的限制?

7 个答案:

答案 0 :(得分: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)方法。

答案 1 :(得分:6)

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

  

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

我认为技术上它可以,并且它不会违反类型替代后的祖先合同(Liskov Substitution Principle)。

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

根据我的分析,我推测/猜测不允许您的方案的理由:

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

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

编制者需要根据您的经验采取行动。的 8.4 Method Declarations

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

中的方法
  • 方法名称相同( 8.4.2 8.4.8.1
  • 在删除泛型类型参数( 8.4.2 8.4.8.1 )之后,方法参数具有相同的类型
  • 返回类型是祖先类中返回类型的type-substitutable,即相同类型或更窄( 8.4.8.3

    注意:subsignature 意味着重写方法使用重写方法的子类型。当覆盖方法具有完全相同类型签名时,覆盖方法被称为具有重写方法的子签名,除了泛型类型和相应的原始类型被认为是等效的。


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

通过多态类型匹配,性能匹配方法签名。通过将覆盖方法签名限制为祖先的精确匹配,JLS将大部分处理移动到编译时。 15.12 Method Invocation Expressions - 摘要:

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

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

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

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

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

  5. 效果

    根据JLS中的文字细分:

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

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

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

答案 2 :(得分:4)

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

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

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

如果Java允许foo(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) { ... }
}

这是demo on ideone

答案 3 :(得分: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
  }
}

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

我希望这会对你有所帮助。

答案 4 :(得分:1)

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

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

答案 5 :(得分:0)

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

答案 6 :(得分: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) { ... }
}

现在,您已成功覆盖原始参数为String的方法,以接受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) { ... }
}