【问题标题】:Class type of reference and the actual class type, which decides which method to call? [duplicate]引用的类类型和实际的类类型,哪个决定调用哪个方法? [复制]
【发布时间】:2017-02-14 12:54:02
【问题描述】:

标题可能具有误导性,但作为非本地人,我想不出更好的标题。

假设我有两个班级,DogFox

public class Dog {
    public String bark() {
        return "Wuff";
    }
    public String play(Dog d) {
        return "Wuff" + d.bark();
    }
}


public class Fox extends Dog {
    public String bark() {
        return "Ringding" ;
    }
    public String play(Fox f) {
        return "Ringding" + f.bark();
    }
}

我创建了一些实例并调用了一些方法

Fox foxi = new Fox();
Dog hybrid = new Fox();
System.out.println(hybrid.play(foxi)); // Output number 1
System.out.println(foxi.play(hybrid)); // Output number 2

对于 1. 输出我期望 "RingdingRingding" 因为 hybrid 实际上是对 Dog 实例的引用,即使引用的类型是 Dog 它仍然引用Fox 对象,但我仍然得到了这个输出:

呜呜呜

第二个我遇到了同样的问题,因为foxiFox 的一个实例,而hybrid 实际上是Fox 的一个实例(不管是什么引用,对吧?),输出应该是@ 987654334@ 但话又说回来,我得到了:

呜呜呜

有人可以解释为什么吗?

【问题讨论】:

  • 重要的是Fox.play(Fox) 不会覆盖Dog.play(Dog)。因为hybrid 的类型是Dog,所以调用了play(Dog) 重载。
  • @AndyTurner 嗨,安迪,感谢您的回答。仍然在类型转换方面,实际类型确实很重要吗?假设我将 (Dog) 转换为 Fox 的一个实例并调用 bark 方法,它仍然是 Fox 类中的方法,对吧?我在参考和铸造之间有点困惑..
  • 要调用的方法是在编译时确定的,而不是在运行时确定的。因此,如果您将对 Dog 的引用传递给方法,则会调用采用 Dog 的重载 - 即使它在运行时引用 Fox
  • @AndyTurner 我真的不明白你说什么。那么为什么当我将(Dog)类型转换为 Fox 的对象时,Fox 中的方法 bark() 仍然会被调用,因为这里对象的实际类型很重要。你有一些关键字,所以我可以了解更多?

标签: java inheritance overloading overriding


【解决方案1】:

方法调用的两个重要事项。

你有两个时间:编译时间和运行时间。
而且这两次的规则也不一样。

  • 在编译时,编译器必须静态确定调用哪个方法的确切签名才能正常编译。
    此绑定是静态的,因为编译器与调用方法的具体实例无关,传递给方法的参数也是一样的。
    编译器不依赖于有效类型,因为在运行时有效类型可能会在执行流程中发生变化。
    所以编译器在可用方法中搜索一个声明类型,根据传递给它的参数的声明类型,这是更具体的方法。

  • 在运行时,将根据调用该方法的有效实例来使用一个类或另一个类的实例方法。
    但是被调用的方法必须遵守编译时指定的签名。

1)对于第一种情况:

Fox foxi = new Fox();
Dog hybrid = new Fox();
System.out.println(hybrid.play(foxi)); // Output number 1
  • 第一次(编译时间):

对于 Dog 实例,编译器必须找到最具体的方法 play(),并将 Fox 声明类型的变量作为参数。

在 Dog 类中,存在具有兼容签名的单个方法 play()

public String play(Dog d) {

所以这个签名用于绑定:String play(Dog d)

关于bark() 方法,很明显,因为只有一个 bark() 方法签名。
所以我们对编译时绑定的方法没有歧义

  • 第二次(运行时):

在运行时调用具体实例的String play(Dog d) 方法。 hybrid 变量引用 Fox 的一个实例,但 Fox 没有覆盖 String play(Dog d)。 Fox 定义了一个 play() 方法,但带有另一个签名:

public String play(Fox f) {

于是JVM调用了Dog的public String play(Dog d) {方法。
然后在d.bark()被执行时调用d有效类型的方法,d引用了Fox实例。

所以输出“WuffRingding”。


2) 对于第二种情况:

Fox foxi = new Fox();
Dog hybrid = new Fox();
System.out.println(foxi.play(hybrid)); // Output number 2
  • 第一次(编译时):

对于Fox 实例,编译器必须找到最具体的方法play(),并将Dog 声明类型的变量作为参数。

Fox 类中,存在两个具有兼容参数的play() 方法:

public String play(Dog d) { // inherited from the parent class

public String play(Fox f) { // declared in Fox

编译器必须为方法的调用上下文选择更具体的方法
它标识了一个比Dog 声明的类型参数的另一个更具体的方法:public String play(Dog d)。 所以编译器在编译类时会将play()方法调用绑定到public String play(Dog d)

  • 第二次(运行时):

在运行时调用具体实例的String play(Dog d) 方法。
对于第一种情况,foxi 变量引用 Fox 的一个实例,但 Fox 不会覆盖 String play(Dog d)
于是JVM调用了Dog的public String play(Dog d)方法。
然后在f.bark()被执行时调用f有效类型的方法,f引用了Fox实例。

于是又输出了“WuffRingding”。


为避免这种意外,您应该添加@Override 在旨在覆盖父类方法的方法中:
例如:

@Override
public String play(Fox f) {
    return "Ringding" + f.bark();
}

如果该方法没有有效地覆盖层次结构中的play(Fox f) 方法,编译器会报错。

【讨论】:

  • 哇,感谢您的回答。仍然尝试了一些类似的东西: Dog a = (Dog) foxi; a.树皮();即使“a”是“Dog”类型的引用,输出仍然是“Ringding”,你能解释一下原因吗?
  • 不客气 :) 重新阅读我的回答的这一部分:“关于 bark() 方法,很明显,因为只有一个 bark() 方法签名。所以我们没有歧义在编译时绑定的方法”。这意味着在编译时唯一的bark() 方法被绑定在编译的类中。并且在运行时,JVM 使用调用该方法的有效实例的绑定方法 (bark())。
  • 我现在明白了,非常感谢!
【解决方案2】:

显然,引起你困惑的是你认为子类Fox中的播放方法覆盖超类的播放方法,而实际上它只是重载它。

如果您将 Fox-class 的播放方法中的 f 的参数类型更改为类型 Dog,由于您在问题中分析的原因,两次输出都将是“RingdingRingding”,因为在这个如果播放方法正确地覆盖了超类方法。

让我们更详细地研究一下重载播放方法的情况:

hybrid.play(foxi)

编译器查看声明的静态类型hybrid,即Dogfoxi 被声明为 Fox。因此,编译器在类Dog 中寻找一个播放方法,它需要一个静态类型的参数Fox。它没有成功,因为只有一个播放方法需要一个静态类型的参数Dog。但是编译器最终还是会选择这个方法来调用,因为DogFox的超类。

foxi.play(hybrid)

编译器查看声明的静态类型foxi,即Foxhybrid 被声明为 Dog。因此,编译器在类Fox 中寻找一个播放方法,它需要一个静态类型Dog 的参数。在类Fox 中有两个播放方法,编译器可以从中选择:play(Fox f) 和继承的重载方法play(Dog d)play(Fox f) 不是一个有效的选择,因为此方法需要一个 Fox 类型的参数,而 hybrid 被声明为 Dog。这意味着编译器将再次选择 play(Dog d) 方法,该方法在类 Dog 中声明,就像前面的语句一样。

编译器不允许您用play(Dog d) 覆盖play(Fox f) 的原因如下:想象一下它是允许的并且有人会这样做:

Dog doggo = new Dog();
hybrid.play(doggo);

现在,重写的 play(Fox f) 方法将在运行时使用 Dog 类型的输入参数调用,这不起作用,因为 play(Fox f) 期望 f 不仅是 Dog,而是更专业的 Fox

为避免重载/覆盖混淆,请使用 @Override 注释应覆盖超类方法的方法。如果带有@Override 注释的方法实际上没有覆盖超类方法,编译器将拒绝编译您的代码。

【讨论】:

    【解决方案3】:

    在您的情况下,play 方法被重载而不是被覆盖。

    当你这样做时Dog d = new Fox()Dog 的引用只会调用Dog 类的方法,除非Dog 类的方法被Fox 类覆盖。当方法被覆盖时,此类方法的调用在运行时解决,但重载方法的调用在编译时解决。

    读取静态多态性和运行时多态性以进一步清除。

    【讨论】:

      【解决方案4】:

      确定将调用哪个方法的规则是pretty complicated,但我将在此处尝试针对这种情况进行总结。

      首先,对于hybrid.play(foxi)

      1. 确定要搜索的类或接口

        hybrid 的类型为Dog,因此将在Dog 接口中搜索方法。这意味着只有您在Dog 上定义的方法才会被调用。它们是:

        bark()
        play(Dog)
        
      2. 确定方法签名

        您正在使用Fox 类型的参数调用play 方法。 FoxDog 的子类型,所以匹配play(Dog) 方法。

      所以,那个方法就是被调用的方法。

      接下来,对于foxi.play(hybrid)

      1. 确定要搜索的类或接口

        foxi 的类型为Fox,因此将在Fox 接口中搜索方法。可用的方法有:

        bark()
        play(Dog)
        play(Fox)
        

        注意play(Fox) 不会覆盖play(Dog):它们没有相同的方法签名,所以play(Fox) 只是一个重载。

      2. 确定方法签名

        您正在使用Dog 类型的参数调用play 方法。因此,play(Dog) 方法是调用的方法,因为它是唯一匹配的方法。

        hybrid 具有运行时类型Fox 并不重要:要调用的方法的这种选择发生在编译时。

        因此,调用play(Dog),而不是play(Fox)

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-10-23
        • 2020-12-20
        • 2018-01-18
        • 1970-01-01
        • 1970-01-01
        • 2017-05-20
        • 2021-09-15
        相关资源
        最近更新 更多