方法调用最关键的问题就是要确定具体调用哪个版本的方法。在Java虚拟机里共有5条方法调用字节码指令:
- invokestatic:调用静态方法
- invokespecial:调用实例构造方法,私有方法和父类方法
- invokevirtual:调用虚方法
- invokeinterface:调用接口方法,在运行时确定一个实现此接口的对象
- invokedynamic
静态解析
通过invokestatic和invokespecial调用的方法在编译器就可以确定调用版本,在类加载的解析阶段,相应的符号引用会被替换为方法的直接引用,这个过程即为静态解析。
对于静态方法来说,它是与类相关的概念,而不是与对象相关的概念。静态方法无法被重写,编译器允许父子类中定义了两个同名的静态方法,但是这两个静态方法分别属于父子类,而不存在重写关系;静态方法可以重载,但仍然可以在编译期唯一确定一个调用版本。(下文再解释,为什么即使重载也可以在编译期确定唯一调用版本)
对于私有方法来说,不具备外部访问条件,即使重载,也可以在编译器确定唯一的调用版本。
分派
根据条件在多个方法版本中选择匹配方法即为分派。主要通过invokevirtual和invokeinterface指令调用。
通常,在B extends A, A a = new B();
中,对于变量a,A称为a的静态类型,B称为a的实际类型。
- 静态分派:通过变量静态类型来定位方法版本,在重载中应用
- 动态分派:通过变量实际类型来定位方法版本,在重写中应用
这里静与动的意思分别指的是编译期和运行时,变量定义时的静态类型在编译期是可知的,而变量的实际类型是编译期不可确定的。如以下情况:
B,C extends A
A a = null;//静态类型可知
if(condition){
a = new B();
}else{
a = new C();
}
//实际类型要在运行时才能确定
静态分派
通过一段代码来说明:
public class Dispatch {
public static void test(Chinese c){
}
public static void test(People p){
}
static class People {
}
static class Chinese extends People {
}
static class English extends People {
}
public void speak(People p) {
System.out.println("undefined");
}
public void speak(Chinese c) {
System.out.println("人");
}
public void speak(English e) {
System.out.println("people");
}
public static void main(String[] args) {
Dispatch d = new Dispatch();
//定义变量p,其静态类型为People,动态类型也为People。
People p = new People();
//同上
Chinese c = new Chinese();
//同上
English e = new English();
// speak方法为Dispatch上的重载方法,通过传入不同静态类型的变量,看编译期如何选择
d.speak(p);
//将c的静态类型强制转换为People
d.speak((People)c);
d.speak(c);
//将e的静态类型强制转换为People
d.speak((People)e);
d.speak(e);
//将静态类型为People的p的实际类型转换为Chinese
p = c;
d.speak(p);
p = e;
d.speak(p);
Dispatch.test(p);//静态方法的选择
}
}
输出为:
undefined//静态类型为People
undefined//c的静态类型被强制转换为People
人//静态类型为Chinese
undefined//e的静态类型被强制转换为People
people//静态类型为English
undefined// 不论实际类型怎么变化,p的静态类型始终为People
undefined
undefined//静态方法的选择一样
可以看到,在选择重载方法的版本时,编译器依据的是传入参数的静态类型(当然也与入参的数量有关),而与其实际类型无关。静态类型是编译期可知的,所以,在静态方法和私有方法的重载中,在编译期也是可以确定调用版本的。
动态分派
还是上文中的代码,添加如下部分:
static class DispatchSon extends Dispatch {
public void speak(People p) {
System.out.println("son undefined");
}
public void speak(Chinese c) {
System.out.println("son 人");
}
public void speak(English e) {
System.out.println("son people");
}
}
main方法:
public static void main(String[] args) {
Dispatch d = new Dispatch();
People p = new People();
Chinese c = new Chinese();
English e = new English();
d.speak(p);
d.speak(c);
d.speak(e);
//将变量d的实际类型改变为DispatchSon类型
d = new DispatchSon();
d.speak(p);
d.speak(c);
d.speak(e);
}
输出:
undefined
人
people
//改变实际类型后调用了子类重写的方法
son undefined
son 人
son people
可以看到,在选择重写的方法时,依据是变量(对象)的实际类型,而不是静态类型。
示例代码字节码
接下来我们在字节码层面看一下,为什么会像上文那样选择,字节码很长,省略了部分代码,将main方法改为:
public static void main(String[] args) {
Dispatch d = new Dispatch();
People p = new People();
Chinese c = new Chinese();
d.speak(p);
d.speak((People) c);
d.speak(c);
p = c;
d.speak(p);
Dispatch.staticSpeak(p);
d.speak(p);
d = new DispatchSon();
d.speak(p);
}
部分字节码如下:
// class version 51.0 (51)
// access flags 0x21
public class Dispatch {
// access flags 0x1
public speak(Dispatch$People) : void
L0
LINENUMBER 53 L0
GETSTATIC System.out : PrintStream
LDC "undefined"
INVOKEVIRTUAL PrintStream.println (String) : void
L1
LINENUMBER 54 L1
RETURN
L2
LOCALVARIABLE this Dispatch L0 L2 0
LOCALVARIABLE p Dispatch$People L0 L2 1
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x1
public speak(Dispatch$Chinese) : void
L0
LINENUMBER 57 L0
GETSTATIC System.out : PrintStream
LDC "\u4eba"
INVOKEVIRTUAL PrintStream.println (String) : void
L1
LINENUMBER 58 L1
RETURN
L2
LOCALVARIABLE this Dispatch L0 L2 0
LOCALVARIABLE c Dispatch$Chinese L0 L2 1
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x9
public static main(String[]) : void
L0
LINENUMBER 79 L0
NEW Dispatch
DUP
//构造方法调用指令invokespecial
INVOKESPECIAL Dispatch.<init> () : void
ASTORE 1
L3
LINENUMBER 83 L3
ALOAD 1: d
ALOAD 2: p
//普通方法调用invokevirtual
INVOKEVIRTUAL Dispatch.speak (Dispatch$People) : void
L4
LINENUMBER 84 L4
ALOAD 1: d
ALOAD 3: c
//这里对应强制转换,可以看到此处调用时speak方法的入参类型为People类型
INVOKEVIRTUAL Dispatch.speak (Dispatch$People) : void
L5
LINENUMBER 85 L5
ALOAD 1: d
ALOAD 3: c
INVOKEVIRTUAL Dispatch.speak (Dispatch$Chinese) : void
L6
LINENUMBER 87 L6
ALOAD 3: c
ASTORE 2: p
L7
LINENUMBER 88 L7
ALOAD 1: d
ALOAD 2: p
INVOKEVIRTUAL Dispatch.speak (Dispatch$People) : void
L8
LINENUMBER 90 L8
ALOAD 2: p
//静态方法调用指令invokestatic
INVOKESTATIC Dispatch.staticSpeak (Dispatch$People) : void
L9
LINENUMBER 92 L9
ALOAD 1: d
ALOAD 2: p
//实际类型为Dispatch时的调用
INVOKEVIRTUAL Dispatch.speak (Dispatch$People) : void
L10
LINENUMBER 93 L10
NEW Dispatch$DispatchSon
DUP
INVOKESPECIAL Dispatch$DispatchSon.<init> () : void
ASTORE 1: d
L11
LINENUMBER 94 L11
ALOAD 1: d
ALOAD 2: p
//实际类型为DispatchSon时的调用
INVOKEVIRTUAL Dispatch.speak (Dispatch$People) : void
L12
LINENUMBER 96 L12
RETURN
L13
LOCALVARIABLE args String[] L0 L13 0
LOCALVARIABLE d Dispatch L1 L13 1
LOCALVARIABLE p Dispatch$People L2 L13 2
LOCALVARIABLE c Dispatch$Chinese L3 L13 3
MAXSTACK = 2
MAXLOCALS = 4
}
在字节码中实际类型不同的调用字节码却是相同的,如何根据实际类型调用重写的方法呢?答案在invokevirtual指令的内部执行逻辑里:invokevirtual指令内部的第一步就是确定调用方法的变量的实际类型,将方法调用的符号引用解析到实际类型的直接引用上。
深入理解Java虚拟机