重载是编译时多态性。真的吗?

24

我知道重写和重载之间的语法差异。我也知道重写是运行时多态,而重载是编译时多态。但我的问题是:“重载真的是编译时多态吗?方法调用是否真的在编译时解决?” 为了澄清我的观点,让我们考虑一个示例类。

public class Greeter {
    public void greetMe() {
        System.out.println("Hello");
    }

    public void greetMe(String name) {
        System.out.println("Hello " + name);
    }

    public void wishLuck() {
        System.out.println("Good Luck");
    }
}

由于所有方法 greetMe()、greetMe(String name)、wishLuck() 都是公共的,它们都可以被重写(包括重载的方法),对吗?例如:

public class FancyGreeter extends Greeter {
    public void greetMe() {
        System.out.println("***********");
        System.out.println("*  Hello  *");
        System.out.println("***********");
    }
}

现在,考虑以下代码片段:

Greeter greeter = GreeterFactory.getRandomGreeter();
greeter.greetMe();
getRandomGreeter()方法返回一个随机的Greeter对象。它可能返回Greeter的对象,也可能返回其任何子类(例如FancyGreeterGraphicalGreeter或其他任何一个)。getRandomGreeter()将使用new关键字创建对象,或者动态加载类文件并使用反射来创建对象(我认为使用反射是可能的),或者使用其他任何可能的方式创建对象。这些Greeter的所有方法都可能被子类覆盖或不被覆盖。因此,编译器无法知道特定方法(重载或非重载)是否被覆盖。对吗?此外,维基百科在虚函数中提到:

在Java中,所有非静态方法默认都是“虚函数”。 除了标有关键字final(不能被覆盖)的方法以及私有方法(无法继承)之外,所有非私有、非final方法都是虚方法。

由于虚函数在运行时使用动态方法分派进行解析,而所有非私有、非final方法都是虚函数(无论是否重载),因此它们必须在运行时解析。对吗?

那么,如何在编译时仍然解决重载?或者,有什么我误解的地方或者我遗漏了什么?

5个回答

15

每个“Greeter”类都有三个虚方法:void greetMe()void greetMe(String)void wishLuck()

当您调用greeter.greetMe()时,编译器可以从方法签名中确定应该调用哪一个三个虚方法 - 即void greetMe()因为它不接受任何参数。调用哪个具体的void greetMe()方法取决于greeter实例的类型,并在运行时解析。

在您的示例中,编译器很容易确定要调用哪个方法,因为方法签名完全不同。用于展示“编译时多态性”概念的稍微更好的示例可能如下:

class Greeter {
    public void greetMe(Object obj) {
        System.out.println("Hello Object!");
    }

    public void greetMe(String str) {
        System.out.println("Hello String!");
    }
}

使用这个问候类将获得以下结果:

Object obj = new Object();
String str = "blah";
Object strAsObj = str;

greeter.greetMe(obj); // prints "Hello Object!"
greeter.greetMe(str); // prints "Hello String!"
greeter.greetMe(strAsObj); // prints "Hello Object!"
编译器会使用编译时类型选择最具体匹配的方法,这就是为什么第二个例子可以工作并调用void greetMe(String)方法的原因。
最后一个调用最有趣:尽管strAsObj的运行时类型是String,但它被强制转换为Object,所以编译器也是这样看待它的。因此,编译器能够找到的与该调用最接近的匹配是void greetMe(Object)方法。

有没有办法让 greeter.greetMe(strAsObj); 打印出 Hello String?换句话说,有没有一种方法可以实现运行时多态性? - tejasvi88

13

如果您想要的话,重载方法仍然可以被覆盖。

虽然重载方法拥有相同的名称,但它们就像不同的家族一样。编译器静态地选择一个给定签名的家族,然后在运行时将其分派到类层次结构中最具体的方法。

也就是说,方法分派分为两步:

  • 第一步在编译时使用静态信息完成,编译器会针对对象声明类型中重载方法列表中与当前方法参数最匹配的签名发出call
  • 第二步在运行时完成,鉴于应该调用的方法签名(记住前面的步骤),JVM将其分派到接收对象的实际类型中最具体的覆盖版本。

如果方法参数类型根本不协变,则重载等同于在编译时对方法名称进行修改;因为它们实际上是不同的方法,所以JVM永远不会根据接收器类型交替调度它们。


我知道重载方法可以被覆盖。但是编译时多态性如何应用于重载方法?它们也可以被覆盖,因此必须在运行时解决。对吗? - Jomoos
请问您能否再详细解释一下这个“family”概念? - Jomoos
1
Jomoos:编译器仍然可以在编译时解析要选择哪个重载方法,因为参数列表不能随着覆盖而改变。在运行时,JVM只是从哪个对象中解析出应该调用该方法的“版本”(在编译时从重载版本中选择)。请原谅我使用了外行人的术语,我没有深厚的计算机科学背景。 - brabec

10

什么是多态性?

我认为:如果一个实体可以用多种形式表示,那么这个实体就被称为具有多态性。

现在,让我们将这个定义应用到Java结构中:

1)操作符重载是编译时多态性。

例如,+操作符可以用于添加两个数字或连接两个字符串。严格来说,这是多态性的一个例子,因为它是在编译时确定的。

2)方法重载是编译时多态性。

例如,同名方法可以有多个实现。这也是编译时多态性。

它是编译时多态性,因为在程序执行之前编译器决定了程序的流程,即在运行时使用哪种形式。

3)方法重写是运行时多态性。

例如,具有相同签名的方法可以有多个实现,这是运行时多态性。

4)基类在派生类位置使用是运行时多态性。

例如,interface引用可以指向其任何实现者。

它是运行时多态性,因为在执行之前无法知道程序的流程,即只有在运行时才能确定使用哪种形式。

希望这可以让你有所了解。


3

在这方面的重载意味着函数类型在编译时被静态确定,而不是动态调度。

实际发生的情况是,对于名称为"foo"且类型为"A"和"B"的方法,创建了两个方法("foo_A"和"foo_B")。哪一个被调用是在编译时决定的(foo((A) object)foo((B) object)会导致调用foo_Afoo_B)。因此,在某种程度上,这是编译时多态性,尽管真正的方法(即采取类层次结构中的哪个实现)是在运行时确定的。


0
我强烈反对将“方法重载”称为“编译时多态”。
我同意“方法重载”是静态绑定(编译时),但我没有看到其中的多态性。
我试图在我的问题中表达我的观点以获得澄清。你可以参考这个链接

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