1、静态分派
首先我们先明确,什么是静态类型,什么是动态类型(也称为实际类型)。
静态类型就是在编译期的时候可知的,动态类型则是在程序运行期间才可以知道的。这里来看一个例子:
public class StaticDispatch {
static abstract class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
public void sayHello(Human human) {
System.out.println("hello,human!");
}
public void sayHello(Man man) {
System.out.println("hello,man!");
}
public void sayHello(Woman woman) {
System.out.println("hello,lady!");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman);
}
}
在这个例子中,Human man = new Man();
Human 就是属于静态类型,Man则属于动态类型。man这个对象在使用的时候,可以为Man,或者是Women,这个是未知的。但是静态类型为Human则是一开始(编译期间)就知道的。
那什么是静态分派呢,所有依赖静态类型来决定方法执行版本的分派动作都称为静态分派。静态分派的典型例子就是重载。所以上面的例子的结果是
hello,human!
hello,human!
重载不会看动态类型,所以都是根据静态类型来分派的,都是进入了Human的参数的方法。
2、动态分派
动态分派和多态的重写息息相关。继续使用上面的例子扩展一下:
public class DynamicDispatch {
static abstract class Human {
protected abstract void sayHello();
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello();
}
}
如果你们学过java的多态特性的话,这里的结果没太多问题,你们都知道是什么了。
man say hello
woman say hello
woman say hello
这里很明显是动态分派了,man和woman都根据了他们的动态类型去选择方法去执行。那这里就有一个问题出现了,man在中间变化了两次类型,那java是如何根据实际类型来分派方法的呢。我们尝试使用javap来寻找一下答案。
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class JvmDemo/DynamicDispatch$Man
3: dup
4: invokespecial #3 // Method JvmDemo/DynamicDispatch$Man."<init>":()V
7: astore_1
8: new #4 // class JvmDemo/DynamicDispatch$Woman
11: dup
12: invokespecial #5 // Method JvmDemo/DynamicDispatch$Woman."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6 // Method JvmDemo/DynamicDispatch$Human.sayHello:()V
20: aload_2
21: invokevirtual #6 // Method JvmDemo/DynamicDispatch$Human.sayHello:()V
24: new #4 // class JvmDemo/DynamicDispatch$Woman
27: dup
28: invokespecial #5 // Method JvmDemo/DynamicDispatch$Woman."<init>":()V
31: astore_1
32: aload_1
33: invokevirtual #6 // Method JvmDemo/DynamicDispatch$Human.sayHello:()V
36: return
0到15都是属于Human man = new Man(); Human woman = new Woman();
这两句代码,意思是创建man和woman这两个对象,并且放到局部变量表中。16和20行分别把局部变量表的第一个变量和第二个对象的引用压到栈顶,这两个对象是将要执行sayHello()方法的所有者。17和21行是方法调用的指令,我们可以看到,这两条指令无论从哪里来看,都是一模一样的。这样看来,问题的原因就出现在了invokevirtual指令本身。
根据《Java虚拟机规范》,invokevirtual指令的运行时解析过程大致分为以下几步:
- 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
- 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常。
- 否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。
- 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
根据这个规范,我们可以知道,invokevirtual指令会根据方法的接受者的实际类型去动态的选择方法版本,这个过程就是方法重写的本质,也就是动态分派。
既然动态分派是跟invokevirtual指令的执行有关,那我们就可以得出一个结论,动态分派跟字段无关,只和方法有关。**事实上,在java里,只有虚方法存在,字段永远都不会是虚的。换句话说就是,字段不参与多态。**当哪个类的方法访问某个字段的时候,该字段指的就是这个类能看到的字段。当父类和子类的字段同名的时候,子类会屏蔽父类的同名字段。
下面再来看一个例子,也就是所谓的面试题:
public class FieldHasNoPolymorphic {
static class Father {
public int money = 1;
public Father() {
money = 2;
showMeTheMoney();
}
public void showMeTheMoney() {
System.out.println("I am Father, i have $" + money);
}
}
static class Son extends Father {
public int money = 3;
public Son() {
money = 4;
showMeTheMoney();
}
public void showMeTheMoney() {
System.out.println("I am Son, i have $" + money);
}
}
public static void main(String[] args) {
Father gay = new Son();
System.out.println("This gay has $" + gay.money);
}
}
结果是:
I am Son, i have $0
I am Son, i have $4
This gay has $2
接下来解析下为什么会这样子输出,两句I am Son是因为隐式调用了一次父类的构造方法,父类的构造器方法里面调用了一次虚方法showMeTheMoney()。实际上执行的版本是Son::showMeTheMoney()方法,因为栈顶是Son对象,所以去选择了Son的虚方法,所以输出是I am Son。第一个输出I am Son的时候,虽然money字段在父类中已经初始化化为2了,但是子类的money还没有被初始化,所以此时的money为0(super方法是第一个执行的)。第二个son的输出相信大家都没有什么问题。第三个money是通过静态类型访问到了父类的money。顺便把javap的指令贴一下:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: new #2 // class JvmDemo/FieldHasNoPolymorphic$Son
3: dup
4: invokespecial #3 // Method JvmDemo/FieldHasNoPolymorphic$Son."<init>":()V
7: astore_1
8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
11: new #5 // class java/lang/StringBuilder
14: dup
15: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
18: ldc #7 // String This gay has $
20: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: aload_1
24: getfield #9 // Field JvmDemo/FieldHasNoPolymorphic$Father.money:I
27: invokevirtual #10 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
30: invokevirtual #11 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
33: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
36: return
参考资料:
《深入理解Java虚拟机》