方法调用-静态分派

/ Jvm / 没有评论 / 327浏览

解析一定是个静态的过程,在编译期间就会被确定,在类装载的解析阶段就会把涉及的符号引用转变为直接引用,而不会延迟到运行期再去完成。 而分派则可能是静态的也可能是动态的,分派主要分为单分派和多分派。 这两类分派方式的两两组合就构成了静态单分派、 静态多分派、 动态单分派、 动态多分派4种分派组合情况,下面我们再看看虚拟机中的方法分派是如何进行的。

在介绍静态分派之前,我们看一下下面重载的例子,然后我们通过例子在详细分析一下虚拟机在实现重载时的具体实现逻辑

源码:

public class StaticDispatch {
static abstract class Human {
}

static class Man extends Human {
}

static class Woman extends Human {
}

public void sayHello(Human guy) {
System.out.println("Human");
}

public void sayHello(Man guy) {
System.out.println("Man");
}

public void sayHello(Woman guy) {
System.out.println("Woman");
}

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
Human

虽然输出结果很多人都知道,但虚拟机为什么会选择执行Human方法的参数呢,为什么不是其它的呢。首先我们看一下下面的代码。

Human man = new Man();

我们把上面代码中的Human称为变量的静态类型,把Man则称为变量的实际类型。静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的。而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。

我们重新回到重载的例子,main()方法中的sayHello()方法调用了两次,那么到底调用哪个重载版本的方法呢。这就取决于传入参数的数量和数据类型。代码中定义了两个静态类型相同但实际类型不同的变量,虚拟机在重载时是通过参数的静态类型而不是实际类型作为判定依据的。 并且静态类型是编译期可知的。所以在编译阶段,虚拟机会根据参数的静态类型决定使用哪个重载版本,所以选择sayHello(Human guy)作为调用目标。

所有通过静态类型确定方法版本的分派动作都称为静态分派。 静态分派的典型应用就是方法重载。 并且确定方法的重载版本的并不是虚拟机而是由编译器确定的,也就是说在代码编译时就已经确定了调用方法的版本。但在有些时候这个重载版本并不是唯一的,所以需要确定一个更加合适的版本。也就是说重载是有优先级的。我们看一下下面的代码。


public class Overload {
public static void sayHello(Object arg) {
System.out.println("Object");
}

public static void sayHello(int arg) {
System.out.println("int");
}

public static void sayHello(long arg) {
System.out.println("long");
}

public static void sayHello(Character arg) {
System.out.println("Character");
}

public static void sayHello(char arg) {
System.out.println("char");
}

public static void sayHello(char... arg) {
System.out.println("char...");
}

public static void sayHello(Serializable arg) {
System.out.println("Serializable");
}

public static void main(String[] args) {
sayHello('A');
}
}
char

结果输出为char这个没有什么需要解释的,因为'A'是一个char类型数据,所以就会匹配char的重载方法。那如果我们把sayHello(char arg)方法注释了那结果会输出什么呢。下面为输出结果:

int

哎,结果居然输出了int这是为什么呢?这是因为这里面执行了一次自动类型转换。字符'A'除了代表一个字符外,还可以标识为一个数字65。也就是字符的hashCode的值。所以当虚拟机重载时没有找到char类型的重载方法时,就会查找int类型的重载方法。下面我们继续注释,这次我们把int重载的方法也注释掉,在看一下输出结果。

long

这次结果输出long的原因是因为除了执行了上述char类型转int类型的操作外,还执行了int类型转long类型的操作。也就是执行了两次自动类型转换。由此可见虚拟机执行重载方法时是有优先级的。具体的优先级为:char->int->long->float->double的顺序转型进行重载方法的匹配。 但不会匹配到byte类型和short类型的重载,因为char到byte类型或short类型的转型是不安全的,它会损失精度。下面我们继续注释掉long类型的重载方法,在一次查看输出结果。

Character

这时在运行时执行了一次自动装箱操作。'A'被包装为了它的包装类java.lang.Character。所以匹配到了Character方法的重载。下面我们继续注释,在看一下输出结果。

Serializable

这个输出确实挺奇怪的,因为char类型貌似和序列化没有什么关系,那结果为什么会输出Serializable呢。原因确实和char类型没有什么关系,但却和char类型的包装类也就是java.lang.Character有关系。因为java.lang.Serializablejava.lang.Character类实现的一个接口。当上述代码自动装箱之后发现还是找不到包装类,但是找到了包装类实现的接口类型,所以又发生了一次自动转型。下面我们继续注释,然后查看结果。

Object

这时是因为char装箱后转型为父类了。也就是转型为java.lang.Object。如果有多个父类,那么在继承关系中从下往上开始搜索,越接近上层的优先级越低。 下面我们继续注释查看结果。

char...

因为这是重载方法中的最后一个了并且正常输出,这就说明可见变长参数的重载优先级是最低的,执行时编译器把字符'A'当做了一个数组元素进行处理了。