Markyan04
Markyan04
发布于 2025-11-22 / 2 阅读
0
0

动态类型?静态类型?(旧文迁移)

题目背景

前段时间有同学发现了一道比较有意思的题。这题似乎来源于CS61B 2021年春季的课程。题目如下:

interface Animal {
    default void greet(Animal a) {
        System.out.println("Hello Animal");
    }

    default void sniff (Animal a) {
        System.out.println("sniff animal");
    }

    default void praise (Animal a) {
        System.out.printlb("u r cool animal");
    }
}

class Dog implements Animal {
    @Override
    public void sniff (Animal a) {
        System.out.println("dog sniff animal");
    }

    void praise(Dog d) {
        System.out.println("u r cool dog");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal a = new Dog();
        Dog d = new Dog();
        
        // try to guess the output
        a.greet(d);
        a.sniff(d);
        d.praise(d);
        a.praise(d);
    }
}

问题分析

先看Animal接口,其是一个具有默认实现的接口,由三个方法构成:

  • void greet (Animal a)

  • void sniff (Animal a)

  • void praise (Animal a)

再看Animal的实现类Dog,经过方法重写和方法重载,一共有四个方法

  • 继承自Animal接口的void greet (Animal a)

  • 重写了Animal接口的void sniff (Animal a)

  • 继承自Animal接口的void praise(Animal a)

  • 重载了praise方法的void praise(Dog d)

最后看main方法,其一共定义了两个对象:

  • a变量是类型为Animal的引用类型,引用的对象为Dog类的实例化对象。a的静态类型为Animal,动态类型为Dog

  • d变量是类型为Dog的引用类型,引用对象为Dog类的实例化对象。d的静态类型和动态类型均为Dog

基于上述分析,我们对代码结构有了基本的认识,下面我将会先介绍正确思路,再给出我一开始踩的坑。

代码输出

a.greet(d)

变量a在编译的时候,认定变量aAnimal对象,因为Dog类已经实现了Animal接口,所以d instanceof Animal恒定为true。基于Animal接口的方法签名,编译器会决定使用方法void greet (Animal a)

在实际执行过程中,a.greet(d)会执行编译时已经决定要执行的方法void greet (Animal a),故输出结果为"Hello Animal"。

a.sniff(d)

类似上一部分,变量a在编译的时候,基于Animal接口的方法签名,编译器会决定使用方法void sniff (Animal a)

但是,在实际执行过程中,由于变量a的引用时Dog类的实例,同时Dog类重写了Animal接口的void sniff (Animal a)。这一操作覆盖掉了原先Animal接口的默认实现,故输出结果为"dog sniff animal"。

d.praise(d)

类似上一部分,变量d在编译的时候,基于Dog接口的方法签名,编译器会决定使用重载了praise方法的void praise (Dog d)

在实际执行过程中,由于变量d的引用时Dog类的实例,故输出结果为"u r cool dog"。

a.praise(d)

(剧透,接下来就是本题卡住我的地方)

变量a在编译的时候,认定变量aAnimal对象,因为Dog类已经实现了Animal接口,所以d instanceof Animal恒定为true。基于Animal接口的方法签名,编译器会决定使用方法void praise (Animal a)

但是,在实际执行过程中,由于变量a的引用时Dog类的实例,不过Dog类却重载了Animal接口的void praise (Animal a)void praise (Dog d),不同于前面,这一操作不会覆盖掉原先的void praise (Animal a)方法,实际执行的依然是void praise (Animal a)方法,故最终输出结果为"u r cool animal"。

分析过程中犯的错

经过上面分析后,似乎得到答案并不是很复杂的事情,然而实际上当时我很完美的给出了错误答案,下面我简单复现一下错误思路的流程。

动态类型和静态类型的检查

先简单讲讲动态类型和静态类型。给自己复习一下。

  • 静态类型:在编译时由变量声明决定的类型,变量的静态类型在整个程序中不会改变。

  • 动态类型:在运行时由实际赋值给变量的对象类型决定,变量的动态类型可以随着赋值的不同而变化。

在第一次接触这个题时,我并没有意识到这个题目的重点是静态类型和动态类型!(来自2025自己:这或许就是大学第一门编程语言课程是Python的劣势所在了,大一的自己居然没有这种最基础的特性的认知。JS和Python这些弱类型语言给自己打上了牢牢的思维烙印)在遇到Animal a = new Dog()时,我第一反应是会发生隐式类型转换,new Dog()所创建的实例将会被自动放大为Animal类型。这导致我误判a.sniff(d)的输出为"sniff animal"。

而实际上,a变量的静态类型为Animal,动态类型为Dog,因此调用的方法会根据动态类型Dog来决定具体实现。

方法签名的匹配和编译时方法的绑定

我们先撇开前面对a.praise(d)的分析,尝试走走另一套逻辑。

现在变量a的数据类型为Dog,你甚至尝试用a.getClass()去确定了一下数据类型。同时,Dog类重载了Animal接口的praise方法为void praise (Dog d),这个函数的签名简直和a.praise(d)完美匹配,那么输出当然是"u r cool dog"!

然后就完美掉进坑里了。

现在讲讲两个基本概念,再给自己复习一下!

  • Java函数重载:它们的调用地址在编译时就绑定了,Java的重载是可以包括父类和子类的,即子类可以重载父类的同名不同参数的方法。这可以被视为一种静态绑定。

  • Java函数重写:只有等到方法调用的那一刻,解释运行器才会确认所调用的具体方法。这可以视为一种动态绑定。

其实运行时执行什么方法,是在编译阶段就已经决定了的。在编译阶段,Java编译器会基于方法签名的匹配,在编译时进行方法的绑定,此时就已经决定了最终会执行什么方法。动态类型只是在运行时决定执行方法的方式。在本题中,出题人藏了一个方法重载而不是方法重写,这一个细微的区别,决定了a.praise(d)并不会像a.sniff(d)一样使用Dog类的方法,因为sniff方法是方法重写,而praise方法是方法重载。


评论