即使对象是子类,也调用超类方法

2022-09-01 15:29:17

我正在玩简单的重载覆盖规则,并发现了一些有趣的东西。这是我的代码。

package com.demo;

public class Animal {

    private void eat() {
        System.out.println("animal eating");
    }

    public static void main(String args[]) {

        Animal a = new Horse();
        a.eat();
    }
}

class Horse extends Animal {
    public void eat() {
        System.out.println("Horse eating");
    }
}

该程序输出以下内容。

动物食用

以下是我所知道的:

  • 由于我们有方法,它肯定不会在子类中访问,因此这里不会出现方法覆盖的问题,因为JLS清楚地定义了它。private void eat()
  • 现在这不是方法重写,它绝对不会从Horse类调用方法public void eat()
  • 现在我们的声明是有效的,因为多态性。Animal a = new Horse();

为什么从类中调用方法?我们正在创建一个对象,那么为什么调用 Animal 类的方法呢?a.eat()AnimalHorse


答案 1

标记的方法无法在子类中重写,因为它们对子类不可见。从某种意义上说,您的类根本不知道有方法,因为它被标记为 .因此,Java 不认为 's 方法是一个重写。这主要设计为安全功能。如果一个类有一个它被标记的方法,则假设该方法应该仅用于类内部,并且外部世界完全无法访问它。如果子类可以重写方法,那么它可能会以意想不到的方式更改超类的行为,这是(1)不预期的,并且(2)潜在的安全风险。privateHorseAnimaleatprivateHorseeatprivateprivate

因为 Java 假定类的方法不会被重写,所以每当您通过某种类型的引用调用方法时,Java 将始终使用引用的类型来确定要调用的方法,而不是使用该引用所指向的对象的类型来确定要调用的方法。这里,引用的类型是 ,因此这是被调用的方法,即使该引用指向 .privateprivateAnimalHorse


答案 2

我不确定我是否理解你的困惑。根据您所知道的:

你是对的,不是覆盖的(因为它是私有的)。换句话说,当你调用 时,不会发生后期绑定,因此,你只是在调用 ,这就是你所看到的。Horse.eat()Animal.eat()anAnimal.eat()Animal.eat()


从你的另一个评论来看,似乎你的困惑是编译器如何决定调用什么。这是一个非常高层次的解释:

当编译器看到 时,它将尝试解析要调用的内容。Animal a =...; a.eat();

例如,如果它看到的是一个静态方法,给出的是一个引用,编译器会将其转换为 call 。eat()aAnimalAnimal.eat()

如果它是一个实例方法,并且遇到一个可能已被子类重写的方法,编译器所做的是,它不会生成调用特定方法的指令。相反,它将生成从 vtable 执行某种查找的指令。从概念上讲,每个对象都有一个小表,其中键是方法签名,值是对要调用的实际方法的引用。例如,如果您的情况不是私有的,则Horse的vtable将包含的内容类似于.因此,在运行时,给定一个引用并被调用,实际上发生的事情是:从引用对象的vtable中查找,并调用关联的方法。(如果 ref 指向 a,则关联的方法将为 )。在大多数情况下,这就是后期装订的魔力。Animal.eat()["eat()" -> "Horse.eat()"]Animaleat()eat()HorseHorse.eat()

对于无法重写的实例方法,编译器执行与静态方法类似的操作,并生成直接调用该方法的指令。

(以上在技术上并不准确,只是一个概念说明,让你了解发生了什么)