方法调用不等同于方法的执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时不设计方法内部的具体运行过程,需要在类 加载期间甚至到运行期间才能确定目标方法的直接引用。
1.解析
所有方法调用的目标方法在class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将一部分的符号引用转化为直接引用。调用目标在程序代码写好,程序进行编译时就必须确定下来,这类方法的调用就称作“解析”。
在Java语言中符合“编译期可知,运行不可变”这个要求的方法,主要包括静态方法和私有方法两大类,前者和类型直接关联,后者在外部不能被 访问,这两类方法的特点决定了它们不能通过继承或者别的方式重写其他版本,因此它们都适合在类加载阶段进行解析。
五条方法调用字节码指令如下:

只有被invokestatic和invokespecial指令调用的方法,都可以在解析阶段确定唯一的调用版本,符合 这个条件的有:静态方法、实例构造方法、私有方法和父类方法,它们在类加载的时候就会把符号引用转为直接引用,这些方法称之为非虚方法,与之相反其他的方法称之为虚方法(final 方法除外)。虽然final方法是 由invokevirtual指令调用,但是它无法被覆盖没有其他的版本,在Java语言规范中明确规定final方法属于非虚方法。
解析调用一定是一个静态的过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号引用全部转化为可确定的直接引用,不会延迟到运行期间再去完成。而分派可能是动态的也可能是静态的。
2.分派
众所周知,Java是一门面向对象的程序语言,因为Java具备面向对象语言的三个基本特性:继承、封装和多态。
2.1静态分配


运行结果:

上面代码中的“Human”称为变量的静态类型(Static Type),或者叫做外观类型(Apparent Type),后面的“Man”则称为变量的实际类型(Actual Type),静态类型和实际类型在程序中都可以发生一些变化,区别是,静态类型的变化仅仅是在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型在编译期是可知的,而实际类型的变化结果在运行期间才可以确定,编译器在 编译程序代码的时候,并不知道对象的实际类型是什么。
虚拟机(准确的是编译器),在重载时是通过参数的 静态类型而不是实际类型作为判断依据的,并且静态类型是编译器可知的,因此在编译阶段,Javac编译器会根据参数的静态类型选择决定使用哪个重载版本。
所有依据静态类型来定位方法执行版本的分配称之为静态分派,静态分派的典型是方法重载,静态分派发生编译期,另外,静态分派虽然能确定出方法的重载版本,但在很多情况下这个重载版本不是唯一的,往往只能确定一个“更加适合”的版本。
2.2动态分派

invokevirtual运行时解析过程大致分为几个步骤:

由于invokevirtual指令执行的第一步就是在运行期间确定接收者的实际类型,所以两次调用invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言方法重写的本质。我们把这种在运行期间根据实际类型确定方法执行版本的分派过程称作动态分派。
2.3 单分派与多分派
方法的接收者与方法的参数统称为方法 的宗量,根据分派居于多少宗量,可将分派划为单分派和多分派,单分派是根据一个宗量针对目标方法 进行选择,多宗量则是根据多于一个宗量对目标方法进行选择。


以上代码编译期间编译器的选择过程(静态分派过程),这是 选择目标方法依据两点:1.静态类型是Father还是Son,2、方法参数是QQ还是360;选择的结果是产生两条invokevirtual指令,因为根据两个宗量进行选择,所以Java语言的静态分派属于多分派。
以上代码运行期间虚拟机的选择过程(动态分派过程),影响方法选择的唯一因素只是方法接受者的实际类型是Father还是Son,因为只有一个宗量作为选择依据,所以Java语言的动态分派属于单分派类型。
网友评论