jvm方法调用 单分派与多分派详解

系统性学习请点击jvm学习目录

本文详细的讲解了什么是单分派与多分派,以及为何静态分派是多分派而动态分派是单分派。这里需要提前有关于静态分派与动态分派的知识。

单分派&多分派 定义

首先来看一下书上给的定义。
方法的接收者(调用者)与方法的参数统称为方法的宗量。
单分派是根据一个宗量对目标方法进行选择(就是具体哪一个方法);而多分派则是根据多于一个宗量来对目标方法进行选择。
抓一下重点,单与多的区别在于一(多)个宗量,也就是一(多)个接收者或参数。看到这,也还是有点懵对吧,不急,我们来看例子。

举例说明

public class Test {
    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 void hardChoice(Object arg) {
            System.out.println("father choose obj");
        }
    }

    public static class Son extends Father {
        @Override
        public void hardChoice(QQ arg) {
            System.out.println("son choose QQ");
        }

        @Override
        public void hardChoice(_360 arg) {
            System.out.println("son choose 360");
        }
		
		@Override
		public void hardChoice(Object arg) {
            System.out.println("son choose obj");
        }
    }

    public static void main(String[] args) {
        Father father = new Father();
        Father son = new Son();
		Object qq = new QQ();
		Object aa = new _360();
        father.hardChoice(qq);
        son.hardChoice(aa);
    }
}

学习了静态分派,我们知道实际上字节码文件就是静态分派之后的结果,在编译期确定目标方法到底是哪一个方法。
那么用静态分派来分析下我们的代码。
qq与aa都是Object类型的变量,它们分别指向QQ实例对象与_360实例对象;而father与son都是Father类型的变量,它们分别指向Father实例对象与Son实例对象。所以在编译期,编译器要把代码编译成字节码文件,就得确定father.hardChoice(qq); son.hardChoice(aa);这两个方法到底用的是哪两个方法(指令总不可能写或许是A方法,或许是B方法吧,总得有个定论)。由于编译期都是按照静态类型来确定的,这里father与son的静态类型都是Father类,而qq与aa的静态类型都是Object类,所以这里两个方法在字节码中将被选择为两条Father.hardChoice(Object arg)方法。
下面我们用javap来看一下这个类的字节码指令解析。验证我们的分析结果。

Code:
       0: new           #2                  // class Test$Father
       3: dup
       4: invokespecial #3                  // Method Test$Father."<init>":()V
       7: astore_1
       8: new           #4                  // class Test$Son
      11: dup
      12: invokespecial #5                  // Method Test$Son."<init>":()V
      15: astore_2
      16: new           #6                  // class Test$QQ
      19: dup
      20: invokespecial #7                  // Method Test$QQ."<init>":()V
      23: astore_3
      24: new           #8                  // class Test$_360
      27: dup
      28: invokespecial #9                  // Method Test$_360."<init>":()V
      31: astore        4
      33: aload_1
      34: aload_3
      35: invokevirtual #10                 // Method Test$Father.hardChoice:(Ljava/lang/Object;)V
      38: aload_2
      39: aload         4
      41: invokevirtual #10                 // Method Test$Father.hardChoice:(Ljava/lang/Object;)V
      44: return

可以看到35,41行指令,都是Method Test$Father.hardChoice:(Ljava/lang/Object;)V
这验证了我们的分析。而从上面的分析中,我们可以看到,在编译期选择方法版本(目标方法)时,静态分派在这里考虑了接收者(调用者)和参数这两个宗量。所以静态分派就是多分派。

下面继续来分析java代码在运行过程中的情况。
当java代码真正运行到father.hardChoice(qq); son.hardChoice(aa);时,JVM会发现,原来father变量指向的是Father类型,son变量指向的是Son类型。此时动态分派只关心接收者,并不关心参数,所以只考虑一个宗量,便是单分派。
???这里你可能会问,为啥就不考虑参数啊,你就没发现qq和aa都不是Object类型吗?
下面来解释这个问题,其实在介绍动态分派时已经提到了答案。动态分派为什么能够在运行过程中发现接收者的实际类型呢?这两个方法在字节码中的指令不是一模一样吗?
答案就在于invokevirtual这条指令。在《java虚拟机规范》中对于该指令的运行时解析过程大致分为这几步:

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

  • 如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IlleagalAccessError异常。

  • 否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。

  • 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

在字节码文件中,我们看到,在invokevirtual指令之前都有一个将局部变量表的变量加载到栈顶。而把注意力放在上面四步的第一步上,我们看到该指令会找到操作数栈顶的元素指向的实际类型。连起来就是找到接收者的实际类型,在实际类型中寻找方法
这就是动态分派的核心所在,所以能够根据实际类型来调用正确的方法,而同时,从上面四步我们也找不到对于参数实际类型的确定,所以可以这样理解,动态分派只考虑接收者,没考虑参数。(换句话说,就是在编译期,参数已经都决定为Object。
所以动态分派是单分派。
按照以上分析,最终的结果应该是Method Test$Father.hardChoice:(Ljava/lang/Object;)VMethod Test$Son.hardChoice:(Ljava/lang/Object;)V,也就是打印出father choose obj和son choose obj
下面是结果
在这里插入图片描述
可以看到,和我们的分析结果一致。

参考资料

  • 周志明《深入理解JAVA虚拟机》
  • https://blog.csdn.net/fan2012huan/article/details/51004615
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值