Java:方法重载

4
我想知道为什么要调用

z.f(-6);

在类M中,in指的是类B中的以下函数:
public void f(double y) {
    this.x = (int) y + B.y;
}

不要在A类中使用函数f,因为b.x已经被A类覆盖。相反,应该使用

public void f (int y) {
    this.x = y*2;
    B.y = this.x;
}

在B类中,至少参数类型匹配。

完整代码如下:

public class A {
    public int x = 1;
    public A(int x) {
        this.x += x;
    }
     public A (double x) {
        x += x;
    }
    public void f(double x) {
        this.x = this.x + (int) (x + B.y);
    }
}

public class B extends A {
    public static int y = 3;
    public int x = 0;
    public B (double x) {
        super((int) x);
    }
    public void f(int y) {
        this.x = y*2;
        B.y = this.x;
    }
    public void f(double y) {
        this.x = (int) y + B.y;
    }
  }

public class M {
      public static void main (String[] args){
        A a = new A(B.y);
        a.f(1);
        B b = new B(3.0);
        A z = b;
        z.f(-5.0);
        z.f(-6);
        System.out.println(b.x + "   " + z.x);
       }
  }

我认为Mike Samuel的回答已经很好地解决了这个问题,因为z是类型A,即使它持有A的子类B的实例,只有A的方法才会被调用;对于子类型B的方法调用,您必须向下转换z(始终使用已检查的向下转换,例如使用instanceof,否则可能会出现运行时异常)。 - 另外,由于我在理论计算机科学上看到了您的个人资料,并考虑在RWTH攻读硕士学位,那里的学习情况如何?从您在TCS上的问题来看,听起来要求相当高? - G. Bach
我误读了Mike Samuel的答案,在他的第一句话中应该是"A.f(double)"。一个正确且不那么详细的答案(同时仍然捕捉到问题)是Miserable Variable的;还要检查那个答案的评论。 - G. Bach
@ G. Bach:我非常推荐亚琛工业大学。它很有挑战性,但也很有趣。我只在我的第一个学期,所以我的经验非常有限;-) - Laura
谢谢您的反馈 :) 出于好奇,你在 TSC 上询问 Eppstein 算法的问题有点课外活动吧?我快要完成本科学位了,除了课程材料(当然不包括发表的文章)之外,直到现在为止都没有必要查看其他资料来撰写我的本科论文。那篇论文是你们的课程作业还是你想更详细地了解一下而去查阅的呢? - G. Bach
@G.Bach:那是关于“算法和数据结构”的专题研讨会的一部分。你在哪里学习?也许我们应该切换到电子邮件? - Laura
4个回答

3
z的静态类型是A,因此z.f(-6)只能绑定到A中的一个方法,这种情况下是A.f(int)
这种语言设计的方式是为了让……
A z = new B(3.0);
z.f(-6);

始终表现与

A z = complicatedWayToComputeTrue() ? new B(3.0) : new A(3.0);
z.f(-6);

如果编译器可以证明 A z 持有一个 B,从而绑定到不同的方法签名,那么这将引入所有种类的非本地语言效果,使得调试或维护Java程序变得非常困难。

想象一下有人试图维护。

final A z = complicatedWayToComputeTrue() ? new B(3.0) : new A(3.0);
// 1000 lines elided
z.f(-6);

将其更改为

A z = new B(3.0);
// 1000 lines elided
z.f(-6);

如果编译器现在可以证明A始终是B,并将Z.f绑定到B中的一个方法,那么维护者将感到困惑。

这个回答的第一句话(也是主要观点)是完全错误的。当超类引用子类的类型时,当然可以在子类上调用方法 - 这就是面向对象编程的本质! - Bohemian
@Bohemian,尝试运行以下程序。class A {} class B extends A { void f() {} public static void main(String[] args) { A x = new B(); A.foo(); } }你将会得到一个编译错误:"无法找到符号方法foo()"。 - Mike Samuel
你们两个都是对的,这取决于情况。如果你在子类中重写一个方法,那么显然它将绑定到父类实例上,如果你重载或创建一个新方法,则不会这样。 - Trevor Freeman
我认为措辞很合理(并且可能可以简单地称之为单分派)。我认为这个问题的答案取决于一个事实,即重载不是覆盖,因此子类中的重载声明对于已声明的父类实例来说不存在。 - Trevor Freeman
@increment1,很好。像“单分派”这样的术语对于向语言设计者解释事情非常有用,但是当回答那些不具备语言背景的Java学习者的问题时,我更喜欢使用在该语言、其库或文档中使用的词语。 - Mike Samuel
显示剩余2条评论

3
z.f(-6);
z 的静态类型是 A,它只有一个名为 f 的方法。该方法接受 double 参数,可以将字面值 -6 提升为参数类型。因此,在编译时,调用被绑定到 A.f(double)
在运行时,发现 z 的实际类型是 B,它重写了 A.f(double) 方法,并使用自己的 B.f(double) 方法进行调用。

我原本以为只有在将变量强制转换为子类时,调用才会绑定到子类方法 - 然而一个简单的测试表明你是正确的,尽管在编译时调用绑定到A.f(double),但在运行时它绑定到B.f(double),因为z是B类型的实例。 - G. Bach
@G.Bach 是的,这就是“覆盖”的全部意义,根据动态类型确定要调用的方法。 - Miserable Variable
我是否理解正确,对于重载方法名的哪个方法被绑定是在编译时决定的,而该(已经选择的)方法的哪个类的覆盖是在运行时决定的?我的尝试是:如果添加一个方法A.f(int),则调用z.(-6)将最终进入B.f(int),因此对我而言,对于该调用,编译器在编译时选择A.f(int),因为z是A类型的变量,而在运行时这被B.f(int)覆盖了。我之前不知道这一点。 - G. Bach

1

Java是单分派的,而你正在尝试做的是双重分派(即调用的方法取决于动态运行时类和参数)。

在Java中,要调用的方法的签名是在编译时确定的;这意味着对象的声明类决定了绑定的方法。在子类中重写方法会影响绑定的实现,但重载方法不会(因为重载的方法具有不同的签名)。

在B类中,你正在使用一个以int为参数的版本重载f()方法,当使用声明为A类的对象时,这个方法似乎不存在(你不能调用它,也不会被调用)。

总结一下:

  • 编译器在编译时绑定到一个方法签名。
  • 这个绑定的签名取决于对象和参数的声明(编译时)类型。
  • 如果一个方法在子类中被重写,那么在子类上调用时(无论声明类型如何),将选择重写的方法。
  • 重载不是重写,要重写需要相同的方法签名(除了协变)。

0

我可能错了,但是如果您将类型为 A 的对象 z 链接到类型为 B 的对象上,它仍然会绑定为类型 B,这就是为什么它执行类 B 中的方法而不是执行类 A 中的方法。请注意,您没有使用 new 创建类型为 A 的对象。

正如Mike Samuel所说,默认情况下,-6 应该被视为一个 int,但这与您的解释不符。我将尝试找到一个合适的答案。


在Java中,-6始终是一个int字面量,而不是“默认值”。Java没有像Go那样的任意精度常量,其中“数字常量表示任意精度的值,不会溢出”。 - Mike Samuel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接