分派
Java是一门面向对象的语言,因为Java具备面向对象的3个基本特征:继承、封装和多态。分派调用过程将会揭示多态性特征的一些最基本的体现,如”重载”和“重写”在Java虚拟机中是如何实现的,虚拟机如何确定正确的目标方法。
1.静态分派
public class Example {
public void sayHello(Human human){
System.out.println("hello,guy");
}
public void sayHello(Man man){
System.out.println("hello man");
}
public void sayHello(Woman woman){
System.out.println("hello woman");
}
public static void main(String[] args) {
Human a = new Man();
Human b = new Woman();
Example example = new Example8_6();
example.sayHello(a);
example.sayHello(b);
}
}
abstract class Human{
}
class Man extends Human{
}
class Woman extends Human{
}
运行结果:
hello,guy
hello,guy
以上方法为什么会选择执行参数类型为Human的重载呢?在解决这个问题之前,我们先按如下代码定义两个重要的概念。
Human man = new Man();
我们把上面代码中的“Human”称为变量的静态类型(Static Type),后面的“Man”则称之为变量的实际类型(Actual Type),静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可以确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。例如下面的代码:
//实际类型变化
Human man = new Man();
man = new Woman();
//静态类型变化
sr.sayHello((Man)man);
sr.sayHello((Woman)man);
解释了这两个概念,样例代码中。main()里面的两次sayHello()方法调用,在方法接受者已经确定是对象“sr”的前提下,使用哪个重载版本,就完全取决于传入参数的数量和数据类型。代码中刻意地定义了两个静态类型相同但实际类型不同的变量,但虚拟机(准确的说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判断依据的。并且静态类型是编译期可知的,因此,在编译阶段,javac编译器会根据参数的静态类型决定使用哪个重载版本,所以选择了sayHello(Human)作为调用目标,并把这个方法的符号引用写到main()方法里的两条invokevirtual指令的参数中。
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是虚拟机来执行的。另外,编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是“唯一的”,往往只能确定一个“更加合适的”版本。
示例如下,感兴趣可以运行(将方法从上往下依次注释掉,查看运行结果)。
public class Example {
public static void sayHello(Object arg){
System.out.println("hello Object");
}
public static void sayHello(int arg){
System.out.println("hello int");
}
public static void sayHello(long arg){
System.out.println("hello long");
}
public static void sayHello(Character arg){
System.out.println("hello Character");
}
public static void sayHello(char arg){
System.out.println("hello char");
}
public static void sayHello(char... arg){
System.out.println("hello char...");
}
public static void sayHello(Serializable arg){
System.out.println("hello Serializable");
}
public static void main(String[] args) {
sayHello('a');
}
}
2.动态分派
了解了静态分派,我们来看下动态分派的过程,它和多态性的另外一个重要体现-重写(Override)有着很密切的关联。实例如下:
public class Example {
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();
}
}
运行结果:
man say hello
woman say hello
woman say hello
那么虚拟机是如何知道要调用哪个方法的?
显然不是根据静态类型来决定,因为静态类型同样都是Human的两个变量man和woman在调用sayHello()方法执行了不同的行为,并且变量man的两次调用中执行了不同的方法。导致这个现象的原因很明显,是这两个变量的实际类型不同,Java虚拟机是如何根据实际类型来分派方法执行版本的呢?我们使用 javap -v 命令获取这段代码的字节码:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class Example$Man
3: dup
4: invokespecial #3 // Method Example$Man."<init>":()V
7: astore_1
8: new #4 // class Example$Woman
11: dup
12: invokespecial #5 // Method Example$Woman."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6 // Method Example$Human.sayHello:()V
20: aload_2
21: invokevirtual #6 // Method Example$Human.sayHello:()V
24: new #4 // class Example$Woman
27: dup
28: invokespecial #5 // Method Example$Woman."<init>":()V
31: astore_1
32: aload_1
33: invokevirtual #6 // Method Example$Human.sayHello:()V
36: return
0~15行的字节码是准备工作,作用是建立man和woman的内存空间、调用Man和Woman类型的实例构造器,将这两个实例的引用存放在第1、2个局部变量表Slot之中,这个动作也就对应了代码中的这两句:
Human man = new Man();
Human woman = new Woman();
接下来的16~21句是关键部分,16、20两句分别把刚刚创建的两个对象的引用压到栈顶,这两个对象是将要执行的sayHello()方法的所有者,称为接收者(Receiver);17和21句是方法调用指令,这两条调用指令单从字节码角度来看,无论是指令(都是invokevirtual)还是参数(都是常量池中第22项的常量,注释显示了这个常量是Human.sayHello()的符号引用)完全一样的,但是这两句指令最终执行的目标方法并不相同,原因就需要从invokevirtual指令的多态查找过程开始说起,invokevirtual指令的运行时解析过程大致分为以下几个步骤:
1.找到操作数栈顶的第一个元素所指向的对象的实际类型,记做C。
2.如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限检验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。
3.否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
4.如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言中方法重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。