总结自:《Java 核心技术第 10 版(套装共 2 册)-凯 S.霍斯特曼 霍斯特曼 科内尔》
下面假设要调用 x.f(args),隐式参数 x[1] 声明为类 C 的一个引用。下面是调用过程的详细描述:
1)编译器查看对象的声明类型和方法名。假设调用 x.f(param),且隐式参数 x 声明为 C 类的对象。需要注意的是:有可能存在多个名字为 f,但参数类型不一样的方法。例如,可能存在方法 f(int) 和方法 f(String)。编译器将会一一列举所有 C 类[2] 中名为 f 的方法和其超类中名为 f 的可访问到的方法[3](超类的私有方法不可访问)。
至此,编译器已获得所有可能被调用的候选方法。
2)接下来,编译器将查看调用方法时提供的参数类型。如果在所有名为 f 的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为重载解析(overloading resolution)。例如,对于调用 x.f("Hello") 来说,编译器将会挑选 f(String),而不是 f(int)。由于允许类型转换(int 可以转换成 double,Manager 可以转换成 Employee,等等),所以这个过程可能很复杂。如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,就会报告一个错误。[4]
至此,编译器已获得需要调用的方法名字和参数类型。
3)如果是 private 方法、static 方法、final 方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法[5],我们将这种调用方式称为静态绑定(static binding)。与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。
4)当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与 x 所引用对象的实际类型最合适的那个类的方法。假设 x 的实际类型是 D,它是 C 类的子类。如果 D 类定义了方法 f(String),就直接调用它;否则,将在 D 类的超类中寻找 f(String),以此类推。
每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表(method table)[6],其中列出了所有方法的签名和实际调用的方法。这样一来,在真正调用方法的时候,虚拟机仅查找这个表就行了。在前面的例子中,虚拟机搜索 D 类的方法表,以便寻找与调用 f(Sting) 相匹配的方法。这个方法既有可能是 D.f(String),也有可能是 X.f(String),这里的 X 是 D 的超类。注意,如果调用 super.f(param),编译器将对隐式参数超类的方法表进行搜索。
比如 Employee 和 Manager 类:
虚拟机为 Employee 和 Manager 两个类生成方法表:
将 e 声明为 Employee 类型。在运行时,由于 getSalary 不是 private 方法、static 方法或 final 方法,所以将采用动态绑定。调用 e.getSalary()的解析过程为:
1)首先,虚拟机提取 e 的实际类型的方法表。既可能是 Employee、Manager 的方法表,也可能是 Employee 类的其他子类的方法表。
2)接下来,虚拟机搜索定义 getSalary 签名的类。此时,虚拟机已经知道应该调用哪个方法。
3)最后,虚拟机调用方法。
在面向对象编程中,x 通常不视为参数,但如果把 f 看作函数,那么 x 及 args 都是这个函数的参数,作为区分,把 x 称为隐式参数,args 称为显式参数。 ↩︎
注意是列举 C 类中的 f 方法而不是列出实际对象中的所有 f 方法,因为对于类型为 C 类的引用,无法调用 C 类子类中新建的方法。 ↩︎
The compiler enumerates all methods called f in the class C and all accessible methods called f in the superclasses of C. ↩︎
因为这些方法都不会被覆盖。注意,如果在父类和子类中声明了相同方法签名的静态方法,将会按隐式参数的声明类型调用对应的静态方法(如果使用父类的类型来调用静态方法,那么将调用父类中的静态方法,另一方面如果使用子类的类型来调用静态方法,则会调用来自子类的方法)。[7] ↩︎
编译后保存在 class 文件中,加载后相关信息进入方法区,以便虚拟机在运行时执行方法 ↩︎