题目背景
前段时间有同学发现了一道比较有意思的题。这题似乎来源于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在编译的时候,认定变量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"。
分析过程中犯的错
经过上面分析后,似乎得到答案并不是很复杂的事情,然而实际上当时我很完美的给出了错误答案,下面我简单复现一下错误思路的流程。
动态类型和静态类型的检查
先简单讲讲动态类型和静态类型。给自己复习一下。
静态类型:在编译时由变量声明决定的类型,变量的静态类型在整个程序中不会改变。
动态类型:在运行时由实际赋值给变量的对象类型决定,变量的动态类型可以随着赋值的不同而变化。
在第一次接触这个题时,我并没有意识到这个题目的重点是静态类型和动态类型!(来自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方法是方法重载。