Java中的静态分派与动态分派

本文是《深入理解Java虚拟机》8.3.2节的读书笔记,理解有误的地方,欢迎指正

首先是两个概念:

  • 静态类型,即是变量声明时的类型。
  • 实际类型,变量实例化时采用的类型。

比如我们有这样一段代码

class Human {}
public class Man extends Human {
    public static void main(String[] args) {
        Human man = new Man();
    }
}

我们就称变量 man 的静态类型为 Human,实际类型为 Man。

静态分派

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派,其典型应用是方法重载(根据参数的静态类型来定位目标方法)。

静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机执行的。

动态分派

在运行期根据实际类型确定方法执行版本。

比如这样一段代码,其中man和woman的静态类型都是Human,但是实际类型各异:

public class DynamicDispatch {
    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();        // Output : man say hello
        woman.sayHello();      // Output : woman say hello
    }
    private static abstract class Human {
            protected abstract void sayHello();
        }
    private static class Man extends Human {
        protected void sayHello() {
            System.out.println("man say hello");
        }
    }
    private static class Woman extends Human {
        protected void sayHello() {
            System.out.println("woman say hello");
        }
    }
}

编译过后所得的字节码文件如下:

(常量池的部分内容)
Constant pool:
   #1 = Methodref          #8.#23         // java/lang/Object."<init>":()V
   #2 = Class              #24            // DynamicDispatch$Man
   #3 = Methodref          #2.#25         // DynamicDispatch$Man."<init>":(LDynamicDispatch$1;)V
   #4 = Class              #26            // DynamicDispatch$Woman
   #5 = Methodref          #4.#25         // DynamicDispatch$Woman."<init>":(LDynamicDispatch$1;)V
   #6 = Methodref          #13.#27        // DynamicDispatch$Human.sayHello:()V
public class DynamicDispatch {
  public DynamicDispatch();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class DynamicDispatch$Man
       3: dup
       4: aconst_null
       5: invokespecial #3                  // Method DynamicDispatch$Man."<init>":(LDynamicDispatch$1;)V
       8: astore_1
       9: new           #4                  // class DynamicDispatch$Woman
      12: dup
      13: aconst_null
      14: invokespecial #5                  // Method DynamicDispatch$Woman."<init>":(LDynamicDispatch$1;)V
      17: astore_2
      18: aload_1                        // 将第二个引用类型本地变量(man)推送至操作数栈栈顶
      19: invokevirtual #6     // Method DynamicDispatch$Human.sayHello:()V,调用#6代表的实例方法,并且方法的接收者就是操作数栈顶元素
      22: aload_2
      23: invokevirtual #6                  // Method DynamicDispatch$Human.sayHello:()V
      26: return
}

在main函数中,0~8是创建Man对象并赋到man,9~17是创建woman对象并赋到woman,18就是将man对象压入操作数栈栈顶,接下来19调用Human的sayHello方法,执行的字节码指令是invokevirtual,其具有多态查找过程,在运行时的解析过程大致为:

  1.  找到操作数栈顶的第一个元素所指向的对象的实际类型,记为C。
  2.  如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果权限校验不通过,返回java.lang.IllegalAccessError异常。
  3.  否则,按照继承关系从下往上一次对C的各个父类进行第2步的搜索和验证过程。
  4.  如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError异常。

这里方法的接收者——即操作数栈栈顶元素是man对象,最后的结果就是调用了Man中的sayHello方法。同样的,对woman对象的方法调用也是如此,最后执行的是Woman中的sayHello方法。

由于invokevirtual指令执行的第一步就是在运行期间确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java方法重写的本质。

posted @ 2019-12-15 09:26  夜读春秋  阅读(199)  评论(0编辑  收藏  举报