方法调用(解析、动态分派、静态分派)

  1. 解析
  2. 分派
    1. 1. 静态分派—(重载)
    2. 2. 动态分派—(重写)
    3. 单分派与多分派
      1. 1. 静态分派
      2. 2. 动态分派
      3. 3. 总结
    4. 动态分派的实现

参考文章:

JVM(十四)方法调用

java方法调用之单分派与多分派(二)

方法调用阶段就是确定被调用方法的版本,即调用哪一个方法。

解析

我们已经知道,class文件中需要调用的方法都是一个符号引用,而在方法调用中的解析阶段,就是要把一部分符号引用转化为直接引用。

能在解析阶段将方法的符号引用转化成直接引用的的方法,必须在方法运行前就确定一个可调用的版本,并且这个版本在运行阶段是不可改变的。

“编译期可知,运行期不可变”,符合这个规则的方法有静态方法和私有方法两大类。前者与所属的类直接关联,后者在外部不可以被访问。这两种方法都适合在解析调用,也就是把这些方法的符号引用转化成直接引用。

与之对应5条调用方法的字节码指令:

  • invokestatic:调用静态方法
  • invokespecial:调用实例构造器方法,私有方法和父类方法
  • invokevirtual:调用所有虚方法
  • invokeinterface:调用接口方法,运行时确定一个实现此接口的对象
  • invokedynamic:先在运行时动态解析出调用点限定符所引用的的方法,然后再执行此方法

只有用invokestatic和invokespecial指令调用的方法(还有final修饰的方法),都可以在解析阶段确定调用版本,这些方法叫做非虚方法。

剩下三个字节码指令调用的方法叫做虚方法(invokevirtual指令调用的final修饰的方法除外)

解析调用是一个静态过程,编译期间就可以确定,解析阶段将符号引用转化为直接引用。

将代码转换为字节码文件:

可以看到,确实是通过invokestatic命令调用sayhello方法。

分派

1. 静态分派—(重载)

先看一段简单的代码:

输出结果是什么?

我们先来分析一下:

首先引入两个概念:我们把代码中的Human成为变量的静态类型,把Man、Woman称为变量的实际类型
静态类型和动态类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候不知道一个对象的实际类型是什么

现在再看代码,对于sd.sayHello(man);直观上看,它似乎传入的是Man类型的参数man,所以应该打印的是man的方法“man is saying hello”,

但是要注意,man 的静态类型仍然是Human,实际类型才是man,所以,在编译阶段,Javac选择了sayHello(Human)作为调用目标。

即最终输出:

通过这个实例,我们可以看到,这里是通过静态类型来定位执行方法的版本,这样的分派动作称为静态分派
静态分派于重载有很深的关系

2. 动态分派—(重写)

先看代码:

生成字节码文件:

注意代码中的:

对应字节码中的:

invokevirtual指令的运行时解析过程大致分为:

(1)找到操作数栈栈顶的第一个元素所指向的对象的实际类型。记作C。

(2)如果在类型C中找到与常量描述符和简单名称相符的方法,就进行访问权限校验,通过则返回方法的直接引用,没有通过则抛出java.lang.IllegalAccessError异常

(3)如果在类型中没有找到对应的方法,则按照继承关系从下往上对C的父类依此查找方法

(4)若始终没有找到合适方法,抛出java.lang.AbstractMethodError异常。

invokevirtual指令执行的时候先确定方法调用的对象的实际类型,所以会把两次方法调用的符号引用解析到不同的直接引用上,这个过程叫做动态分派,是方法重写的本质。

代码结果为:

单分派与多分派

先看一段代码:

public class Dispatcher {
    static class QQ {}
    static class _360 {}

    public static class Father {
        public void hardChoice(QQ arg) {
            System.out.println("father choose QQ");
        }
        public void hardChoice(_360 arg) {
            System.out.println("father choose _360");
        }
    }
    public static class Son extends Father {
        @Override
        public void hardChoice(QQ arg) {
            System.out.println("son choose QQ");
        }
        public void hardChoice(_360 arg) {
            System.out.println("son choose 360");
        }
    }
    public static void main(String[] args) {
        Father father = new Father();
        Father son = new Son();
        father.hardChoice(new _360());
        son.hardChoice(new QQ());
    }
}

运行结果是什么?

它的字节码文件:

我们分别从静态分派动态分派的角度来分析

1. 静态分派

看字节码的

24: invokevirtual #8                  // Method 单分派多分派/Dispatcher$Father.hardChoice:(L单分派多分派/Dispatcher$_360;)V
35: invokevirtual #11                 // Method 单分派多分派/Dispatcher$Father.hardChoice:(L单分派多分派/Dispatcher$QQ;)V

对应的是代码的:

father.hardChoice(new _360());
son.hardChoice(new QQ());

可以看到,invokevirtual相同,调用的都是$Father.hardChoice方法,只是他们的参数不同。

这说明他们的静态类型相同,都是Father。在选择目标方法时,根据两个宗量,是多分派的,即静态分派属于多分派类型

宗量:方法的接收者和方法的参数统称为方法的宗量。

单分派:根据一个宗量对目标方法进行选择

多分派:多于一个宗量对目标方法进行选择

2. 动态分派

当在执行

father.hardChoice(new _360());
son.hardChoice(new QQ());

发现son的实际类型是Son,所以转去调用Son的方法。在father中也执行了此过程,只不过,father的实际类型仍然是father。

目标选择时只依据了一个宗量,是单分派的。因此,动态分派属于单分派类型

3. 总结

静态分派关注了两个宗量,即静态类型和参数,(参数对比重载)

而动态分派关注实际类型,(对比重写)

java语言是一个静态多分派,动态单分派的语言

动态分派的实现

动态分派类似重写,动态分派是非常频繁的动作,运行时需要在元数据中搜索合适的目标方法,最常用的“稳定优化”手段就是建立一个虚方法表,存放各个方法实际入口地址。

子类重写了方法,就会指向子类的方法地址。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 2470290795@qq.com

文章标题:方法调用(解析、动态分派、静态分派)

文章字数:1.7k

本文作者:runze

发布时间:2020-01-28, 18:58:38

最后更新:2020-01-29, 11:17:22

原始链接:http://yoursite.com/2020/01/28/JVM/JVM%E5%AD%97%E8%8A%82%E7%A0%81%E6%89%A7%E8%A1%8C%E5%BC%95%E6%93%8E%EF%BC%88%E4%BA%8C%EF%BC%89%E2%80%94%E2%80%94%E6%96%B9%E6%B3%95%E8%B0%83%E7%94%A8%EF%BC%88%E8%A7%A3%E6%9E%90%E3%80%81%E5%8A%A8%E6%80%81%E5%88%86%E6%B4%BE%E3%80%81%E9%9D%99%E6%80%81%E5%88%86%E6%B4%BE%EF%BC%89/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏