Java中的动态方法分派是如何工作的?

5

一个超类变量能否访问子类重写的方法?例如:

class A {
    void callMe() {
        System.out.println("Inside A");
    }
}

class B extends A {
    void callMe() {
        System.out.println("Inside B");
    }
}

class Dispatch {
    public static void main(String args[]) {
        A a = new A();
        B b = new B(); // Object of type B
        A r; // Obtain a reference of type A

        r = a; // Refers to A object
        r.callMe(); // Calls A's version of callMe()

        r = b; // Refers to B object
        r.callMe(); // calls B's version of callMe() and my question is on this
    }
}

我前面学到的是,一个引用子类对象的超类变量只能访问由超类定义的那些对象部分。那么第二个 r.callMe() 怎么会调用 B 版本的 callMe()呢?它应该再次调用 A 版本的 callMe()


不应该。由于对象是 B(无论引用是什么),将调用对象的方法。 - Kayaman
最初,r 指向 A 的引用,因此它与 A 相同,然后 r 指向 B 的引用,因此它调用了 Bcallme() - Haris Qurashi
也许这篇关于动态类型和静态类型的帖子会有所帮助。 - Stefan Warminski
1
callme()在A中定义。但是它在B中被覆盖了,因此由于r的类型为B,在B中重写的方法被调用。这就是多态的全部含义。如果你只在B中定义了foo()方法,那么编译器不会允许你调用r.foo(),因为在A中没有定义foo()方法,并且r的声明类型为A。 - JB Nizet
5个回答

2

如果一个超类变量引用了子类对象,那么只能访问由超类定义的对象部分

这并不完全正确。最终,在运行时调用实际对象类型,而不管引用类型是什么。因此,r.callme()实际上将调用在B中定义的callme(),因为rB对象。

new B();       // <-- The object in memory is of type B and its type never
               //         changes.
A a = new B(); // <-- The object in memory is of type B; the reference type
               //         is A. But that effectively does only matter at
               //         compile-time, I believe.

在上面的例子中,B 被称为对象类型,A 被称为引用类型。
请参见 Java 语言规范 § 15.12.4.4

X 为方法调用目标引用的编译时类型。

[...]

如果调用模式为虚拟,并且声明在 S 中覆盖了 X.m§8.4.8.1),则在 S 中声明的方法是要被调用的方法,并且该过程终止。


让我猜一下他们所说的“仅访问由超类定义的那些部分”的含义:
class A {
    void doSomething() { }
}
class B extends A {
    void doAnotherThing() { }
}

A a = new B();
a.doAnotherThing(); // Not valid, because doAnotherthing()
                    // is defined in class B.

为了调用doAnotherThing(),必须使用类型转换:
((B) a).doAnotherThing(); // Valid

1

在你的问题中

r=b;

现在r捕捉到了"new B()"对象。当你调用r.callme()时,会运行B类中的callme方法。因为r有B对象。

任何程序都会抛出编译时错误,因为超类的引用类型没有子类名称的方法。

就像这个例子

class Animal {
  public void move() {
     System.out.println("Animals can move");
  }
}

class Dog extends Animal {
  public void move() {
     System.out.println("Dogs can walk and run");
  }

  public void bark() {
     System.out.println("Dogs can bark");
  }
 }

 public class TestDog {

  public static void main(String args[]) {
    Animal a = new Animal();   // Animal reference and object
    Animal b = new Dog();   // Animal reference but Dog object

    a.move();   // runs the method in Animal class
    b.move();   // runs the method in Dog class
    b.bark();
 }
}

输出

TestDog.java:26: error: cannot find symbol
  b.bark();
   ^
 symbol:   method bark()
 location: variable b of type Animal
 1 error

所以子类的重写方法可以被超类的对象调用,但是没有被重写的方法不行……对吧? 另外,如果我们已经知道要调用哪个函数,为什么还说这个决定是在运行时做出的呢……? - Akash Singh

0

我认为你可能会在对象和变量之间有些混淆。变量可以被看作是一个“指针”。它所做的就是指向一个对象。
例如:

A var = newA(); // var -> Object A

即使var1的类型被定义为A,它仍然可以指向该类型的子类。
例如:

A var = new B(); // var -> Object B

现在,当您在变量上调用方法时,它会在指向的任何对象上调用该方法(即使该对象是子类)。 例如:
A var = new B(); // var -> Object B
var.someMethod(): // calls B.someMethod()

尽管var是类型A的,调用它上面的方法仍会调用B.someMethod(),因为var指向Object B

变量的类型只定义了该变量可以指向哪些对象,所以在我们的情况下,所有的var都是类型A,这意味着它只能指向类型为A或扩展A的对象。

希望这有所帮助!:)


0

因为这是Java中方法查找的工作方式。

  • 首先,访问变量。在您的情况下是r
  • 然后,找到存储在变量r中的对象,即b
  • 找到对象的类,即B
  • 搜索类以匹配方法。现在,B包含callMe()方法。因此,执行此方法而不是超类callMe()方法。如果未覆盖callMe(),则将执行AcallMe()方法。

此外,这对于多态性是必需的。例如: 假设您有一个超类Fruit和子类Apple和Grape。

public class Fruit{
    public String getName() {
        return "Fruit";
    }
}
public class Apple extends Fruit{
    private String name;
    public Apple(String name){
        this.name = name;
    }
    public String getName(){
        return name;
    }
}
public class Grape extends Fruit{
    private String name;
    public Grape(String name){
        this.name = name;
    }
    public String getName(){
        return name;
    }
}

现在,您想要将苹果和葡萄的实例存储在同一个列表中。

Fruit apple = new Apple("apple");
Fruit grape = new Grape("grape");

ArrayList<Fruit> fruits = new ArrayList<Fruit>();
fruits.add(apple);
fruits.add(grape);

现在,您想要遍历每个水果项目并获取它们的名称。

fruits.forEach(item -> {
    System.out.println(item.getName());
});

在这种情况下,项目的类型是水果,并调用getName:您希望从相应的子类Apple/Grape中获取名称,而不是从超类Fruit中获取"Fruit"值。

0
Java方法是虚拟的,因此对象的运行时(实际)类型决定了调用哪个方法,而不是变量的静态(声明)类型。
然而,变量的静态(声明)类型决定了哪些方法(以及字段)是可见的,即你可以调用哪些方法。

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