Java语言中多态的机制
1. 方法解析
Class文件的编译过程中,不包含传统编译中的连接步骤,一切方法的调用在Class文件中存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址。
这个特性给java带来了更强大的动态扩展能力,使得可以在类运行期间才能确定某些目标方法的直接引用,称为动态连接。
也有一部分方法的符号引用在类加载阶段或第一次使用时转化为直接引用,称为静态解析。
静态解析成立的条件是:方法在程序真正执行前,就有一个可以确定调用的版本,并且这个方法的调用版本在运行期是不可变的
。换句话说,调用目标在编译器进行编译时就必须确定下来
。这类方法的调用称为解析。
在Java语言中:符合“编译器可知,运行期不可变”这个条件的方法主要有静态方法和私有方法两大类。前者直接关联类型,后者在外部不可被访问。因此均不会出现通过继承或别的方式重写出其他版本的情况,从而都适合在类加载阶段进行解析。
Java虚拟机中,提供了4中方法调用字节指令,分别是
invokestatic
:调用静态方法invokespecial
:调用实例构造器init
方法、私有方法、父类方法。invokevirtual
:调用所有的虚方法invokeinterface
:调用接口方法,会在运行时确定一份实现此接口的对象
对于被1.2.两个指令调用的方法,都可以在解析阶段确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器和父类方法四类。它们在类加载时就会把符号引用解析为该方法的直接引用。这些方法称为非虚方法。非虚方法还包括被final
修饰的方法。
对于final
修饰的方法,虽然调用final
方法使用的是invervirtual
指令,但是由于被final
修饰的方法无法被覆盖,不会存在其他版本,所以也无需对方法接收者进行多态选择。
Java语言中,也明确规定了被fianl
修饰的方法是一种非虚方法。
解析调用,一定是一个静态过程。在编译期间就会完全确定,在类加载的解析阶段就会把涉及的符号引用转化为可以确定的直接引用,不会延迟到运行期再去完成。
而分派调用则可能既是动态的,也可能是静态的。根据分派一句的宗量数,也可以分为单分派和多分派。两类分派方式两两结合便构成了静态单分派、静态多分派、动态单分派、动态多分派四种分派情况。
2. 静态分派
所有依赖静态类型来定位方法执行版本的分派动作,都称为静态分派。
静态分派的最典型的应用就是多态性中的方法重载。
静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。
Human man = new Man();
对于以上代码,Human
称为变量的静态类型,后面的Man
称为变量的实际类型。静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译器可知的,而实际类型变化的结果在运行期才可确定。
在调用方法时,方法的调用者为同一个静态类型时,调用哪一个重载版本完全取决于传入参数的数量和数据类型。编译器(不是虚拟机,因为根据静态类型做出判断,那么在编译期间就确定了)在重载时是通过参数的静态类型而不是实际类型作为判断依据的。并且,静态类型在编译期间是可知的,所以在编译阶段,javac编译器就根据参数的静态类型做出选择,决定使用哪一个重载版本。这就是静态分派的最典型应用。
3. 动态分派
动态分派与多态性的另一个重要体现——方法覆写由很紧密的联系。
向上转型调用子类的覆写方法便是一个很好的动态分派的例子。这种情况很常见,显然在判断执行父类中的方法还是子类中覆盖的方法时,如果用静态类型来判断,那么不论怎么进行向上转型,都只会调用父类中的方法。但实际情况是,根据父类实例化的子类不同,会调用不同子类的覆写方法,这是需要根据变量的实际类型来分派方法的执行版本的。
而实际类型的确定需要在程序运行时才能确定下来,这种在运行期根据实际类型确定方法执行版本的过程分派称为动态分派。
4. 单分派和多分派
单分派是根据一个宗量对目标方法进行选择,多分派是根据多于一个宗量对目标方法进行选择。
方法的接受者(即方法的调用者)于方法的参数统称为方法的宗量。
在编译期间,编译器的选择过程,即静态分派过程。这时选择目标方法的依据有两点:
- 方法的接受者(调用者)的静态类型
- 方法参数类型
因此是根据两个宗量来进行选择的。所以Java语言的静态分派属于多分派类型。
在运行阶段,虚拟机选择的过程,即动态分派过程。这时候由于编译期已经确定了目标方法的参数类型(编译期根据参数的静态类型进行静态分派),因此唯一可以影响到虚拟机选择的因素只有一个:
- 此方法的接受者(调用者)的实际类型
因为是根据一个宗量来进行选择的。所以Java语言的动态分派属于单分派类型。
5. 总结
Java语言的多态实现机制:
- 方法重载
- 在编译期间,由编译器执行的静态分派,属于多分派类型,分派宗量有:
- 方法接受者(调用者)的静态类型
- 方法参数列表
- 在编译期间,由编译器执行的静态分派,属于多分派类型,分派宗量有:
- 方法重写(方法覆写)
- 在运行期间,由虚拟机执行的动态分派,属于单分派类型,分派宗量为:
- 方法接受者(调用者)的实际类型。
- 在运行期间,由虚拟机执行的动态分派,属于单分派类型,分派宗量为: