Markyan04
Markyan04
发布于 2024-12-11 / 82 阅读
2
3

【开发杂谈1】动态类型?静态类型?

题目背景


前段时间有同学发了一道比较有意思的题。
这道题的来源似乎是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.println("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,动态类型为Dog。

基于如上分析,我们对代码结构有了基本的认识。为了确保文章完整性,下面的解答中,会先使用最终正确的思路回答问题,再给出我一开始所踩的坑(给我坑的一愣一愣的)。

代码输出

a.greet(d)

变量a在编译时,认定变量a是Animal对象,由因为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在编译阶段,认定变量a是Animal对象,由因为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"。

分析过程中可能犯的错

经过上面的分析后,似乎得到最终答案并不是很复杂的事情,然而实际上我前前后后被这个题折磨了两次......下面我简单复现并分享一下错误思路的全流程。

(1)动态类型检查和静态类型检查

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

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

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

在第一次接触这个题时,我居然没有意识到动态类型和静态类型。(没错,这个编程语言最基本的特性我居然没了解过。这或许就是大学第一门编程语言课程是Python的劣势)在遇到Animal a = new Dog();时,我第一反应是会发生隐式类型转换,new Dog()所创建的实例将会被自动放大为Animal类型。这导致我误判a.sniff(d)的输出为"sniff animal"。

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

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

我们先撇开先前对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方法是方法重载。


评论