方法派发

  1. 静态分派
  2. 动态分派

分派即指的是找到要调用的方法的版本, 也就是调用哪个方法.

在Java中方法分派分为俩部分

  • 静态分派
  • 动态分派

静态分派

静态分派指的是在编译期便能决定调用哪个方法版本, 例如

  • invokestatic 指令 : 调用静态方法
  • invokespecial 指令 : 调用私有, 构造器, 父类方法
  • invokevirtual 指令: 调用实例方法
  • invokeinterface 指令: 调用接口方法

咦, 所有的指令(刨除invokedynamic) 都用了哈, 的确是是都用了, 但是静态分派一大应用场景是方法重载

1
2
3
4
5
6
7
8
9
10
11
12
public void run() {
A a = new A();
A b = new B();
pring(a);
pring(b);
}

public void pring(A a) { }
public void pring(B b) { }

static class A { }
static class B extends A {}

上面这个就是一个典型的静态分派场景, 俩个pring方法调用, 都会调用pring(A a).

编译器在重载时是通过参数的静态类型而不是实际类型作为判定的依据。并且静态类型在编译期可知,因此,编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本。
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用就是方法重载。
静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的,而是由编译器来完成。

动态分派

动态分派主要是下面这俩个指令实现

  • invokevirtual 指令: 调用实例方法
  • invokeinterface 指令: 调用接口方法

动态分派是由于在运行期不能确定, 该调用哪个版本的方法, 也就是不确定应该调用哪个类实现的方法,因此需要在运行期动态决定, 这一典型应用就是方法重写

1
2
3
4
5
6
7
8
9
10
11
12
13
 0: new           #2                  // class Test$A
3: dup
4: invokespecial #3 // Method Test$A."<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method Test$A.print:()V
12: new #5 // class Test$B
15: dup
16: invokespecial #6 // Method Test$B."<init>":()V
19: astore_2
20: aload_2
21: invokevirtual #4 // Method Test$A.print:()V
24: return

在invokevirtul的时候, 其具有多态查找过程,在运行时的解析过程大致为:

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

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

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