欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

简单介绍Java的静态分派和动态分派

程序员文章站 2022-07-23 21:36:03
最近复习JVM的知识,对于静态分派和动态分派的理解有点混乱,于是自己尝试写写代码,在分析中巩固知识。 有如下一段代码,请问每一段分别输出什么? 下面我简单地介绍一下从代码编译到方法调用的整个过程。 · 编译 先看看第1段输出,child.foo()是调用父类还是子类的静态方法呢? 在编译阶段,发生了 ......

最近复习jvm的知识,对于静态分派和动态分派的理解有点混乱,于是自己尝试写写代码,在分析中巩固知识。

有如下一段代码,请问每一段分别输出什么?

 1 package com.khlin.my.test;
 2 
 3 class base {
 4 
 5     public static void foo() {
 6         system.out.println("base.foo() invoked");
 7     }
 8 
 9     public void bar(int c) {
10         system.out.println("base.bar(int) invoked");
11     }
12 
13     public void bar(character c) {
14         system.out.println("base.bar(character) invoked");
15     }
16 
17     public void baz(object o) {
18         system.out.println("base.baz(object) invoked");
19     }
20 
21     public void baz(integer i) {
22         system.out.println("base.baz(integer) invoked");
23     }
24 
25 }
26 
27 class child extends base {
28     public static void foo() {
29         system.out.println("child.foo() invoked");
30     }
31 
32     public void bar(character c) {
33         system.out.println("child.bar(character) invoked");
34     }
35 
36     public void bar(char c) {
37         system.out.println("child.bar(char) invoked");
38     }
39 }
40 
41 public class app {
42 
43     public static void main(string[] args) {
44         base child = new child();
45 
46         system.out.println("第1段输出:");
47         child.foo();
48         child.bar(new character('c'));
49 
50         system.out.println("第2段输出:");
51         object integer = new integer(100);
52         child.baz(integer);
53 
54         system.out.println("第3段输出:");
55         child.bar('c');
56 
57     }
58 }

 

下面我简单地介绍一下从代码编译到方法调用的整个过程。

· 编译

先看看第1段输出,child.foo()是调用父类还是子类的静态方法呢?

在编译阶段,发生了静态分派

1 base child = new child();

在我们创建一个对象时,如上图,base称为变量的的静态类型(static type), 或者叫做外观类型(apparent type),后面的child则称为变量的实际类型(actual type)。

所有依赖静态类型来定位方法执行版本的分派动作,称为静态分派。静态分派的典型应用是方法重载,其发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。

方法的接收者(reciever) 和方法的参数统称为方法的宗量,根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。

在静态分派的时候,选择目标方法的依据有两点,一是静态类型是base还是child,二是方法的参数类型。因此,静态分派是多分派。

接下来,我们来看看“第1段输出”代码生成的指令。通过javap -v app.class指令得出如下结果,可以看到第18和第31行两条指令的符号引用,和上述分析一致:child的静态类型是base,所以选择base类的方法;通过无参数和character类型,分别确定是具体哪个方法版本。

简单介绍Java的静态分派和动态分派

但最终两者的行为不一样,child.foo() 调用的是静态类型base的foo(),而child.bar(new character('c')) 则是调用实际类型child的方法。

原因就是出在两条指令不一样:invokestatic和invokevirtual

在java虚拟机里面提供了5条方法调用字节码指令:

invokestatic:调用静态方法

invokespecial:调用实例构造器<init>方法、私有方法和父类方法

invokevirtual: 调用所有的虚方法

invokeinterface:调用接口方法,会在运行时再确定 一个实现此接口的对象

invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在java虚拟机内部的,而invokedynamic是由用户所设定的引导方法决定的。

具体原因是不同的指令在下一阶段(类加载的解析)的行为不一样,暂时先放到一边,我们再看看第2段输出的指令。

简单介绍Java的静态分派和动态分派

可以看出,在静态分派时,是根据传入方法的参数的静态类型来决定调用的方法版本,虽然有baz(integer)的方法,但是传入的参数integer的静态类型是object,所以调用了baz(object)。

再来看看第3段输出的指令,我们知道符号引用肯定还是base类里的方法(尽管child类里有参数一样的bar(char c) 方法),但base里没有一模一样参数(char类型) 的方法,不会报错吗?会调用哪个方法呢?

简单介绍Java的静态分派和动态分派

原来,编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是“唯一的”,往往只能确定一个“更加合适”的版本。

具体可参考:https://www.cnblogs.com/kingsleylam/p/6789119.html

· 类加载之解析

 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法,私有方法,实例构造器,父类方法4类,它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法可以称为非虚方法,其他方法称为非虚方法(除了final方法)。

final修饰的方法,虽然是使用invokevirtual指令来调用,但由于它无法被覆盖,没有其他版本,因此也是非虚方法。具体参考:https://www.cnblogs.com/kingsleylam/p/6765426.html

回到第1段输出,child.foo()是invokestatic指令,那么在解析阶段,就会替换成直接引用,具体的类也就确定下来了,因此调用的是静态类型base.foo()。

而child.bar(new character('c')) 是invokevirtual, 在这个阶段可以确定调用的方法签名,但还不能确定方法的接收者的实际类型。它将由动态分派来完成确定。由于只有一个宗量影响,因此动态分派是单分派。

方法接收者的实际类型在下一阶段确定。

· 运行期的方法调用

在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。

具体可以参考:https://www.cnblogs.com/kingsleylam/p/6789989.html

最终输出结果是:

简单介绍Java的静态分派和动态分派

 

参考资料:《深入理解java虚拟机》第2版 周志明著