方法调用并不等于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),占式还不涉及方法内部的具体运行过程。
程序运行中,方法的调用是最频繁、最普遍的操作,但Class文件编译的时候并不包含传统的连接步骤,而是保存符号引用。然后在类加载甚至运行时把能确定目标方法的直接引用。这样的机制让Java有了更强大的动态扩展能力,但也使得Java方法调用过程变得相对复杂起来。
在类加载的解析阶段会将常量池中的符号引用的一部分转化为直接引用。这种解析能成立的条件是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期间是不可改变的。这类方法的调用称为解析。
在Java语言中符合”编译期可知,运行期不可变“这个要求的方法,主要包括静态方法和私有方法两大类,前者和类型直接关联,后者在外部不可访问,它们各自的特点决定它们都不可能通过继承或别的方式重写其他版本,因此它们适合在类加载阶段进行解析。
解析调用一定是个静态的过程,在编译期间就可以完全确定,在类装载的解析阶段就会把涉及的符号引用转化为直接引用,不会延迟到运行期进行。而分派调用则可能是静态的也可能是动态的。而且根据分派的宗量数还可以分为单分派和多分派。
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此静态分派的动作实际上不是由虚拟机来执行的。
”Human man = new Man()"中,Human称为静态类型,Man称为实际类型上面代码中,已经确定方法接收者是"sr"的情况下,使用哪个重载版本完全取决于传入的参数数量和类型。代码中刻意定义了两个静态类型相同实际类型不同的变量,但虚拟机(准确的说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译期已知的,而实际类型在运行期才能确定。
动态分派和多态性的另一个重要体现----方法重写有着密切的联系。动态分派是看接收者的实际类型而非静态类型,和静态分派相反。
方法的接收者与方法的参数统称为方法的宗量。根据分派基于多少种宗量,可以划分为单分派和多分派两种。
首先来看编译阶段编译器选择过程,也就是静态分派过程。这时选择目标方法的依据有两点:一是静态类型是Father还是Son,二是参数是Write还是Black。最后编译器的出的结论是:Father.choose(Write)和Father.choose(Black)。因为是根据两个宗量选择,所以Java语言的静态分派属于多分派类型。
再看看运行阶段的虚拟机选择,也就是动态分派过程。在执行Son.choose(Black)时,由于编译期已经决定目标方法的参数是Black,虚拟机现在不关心参数的类型,只关注此方法的接收者的实际类型,这里实际类型是Son。因为只有一个宗量进行选择,所以Java语言的动态分派属于单分派类型。