首先来看以下代码的运行结果
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("hello, guy");
}
public void sayHello(Man guy) {
System.out.println("hello, gentleman!");
}
public void sayHello(Woman guy) {
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);
}
}
上述代码的输出结果是
hello, guy
hello, guy
我初次看到这段代码的时候,怎么也想不到这段代码的结果会是如上结果。后来看了解析,发现这里涉及到Java中的一个概念–分派。
Java具有面向对象的三个基本特征:封装、继承和多态,而分派,是多态性特征(如“重载”、“重写”)在虚拟机层面的一种体现。具体来说,分派是虚拟机确定正确的目标方法的一个过程。
首先来了解一下静态类型和实际类型这两个概念
Map map = new HashMap<String, String>();
上例中的Map叫静态类型/外观类型,HashMap叫做实际类型。可以看出来,静态类型一般为抽象类/基类/接口,实际类型一般为子类/实现类。
静态类型和实际类型在程序中都可以发生一些变化, 区别是静态类型的变化仅仅在使用时发生, 变量本身的静态类型不会被改变, 并且最终的静态类型是在编译期可知的; 而实际类型变化的结果在运行期才可确定, 编译器在编译程序的时候并不知道一个对象的实际类型是什么。
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。
静态分派的典型应用是方法重载。当在一个发生重载的方法中寻找最匹配的执行方法时,会优先根据静态类型来选择方法。现在再回头看我们的引导案例,实际执行方法为什么是*sayHello(Human guy)*就一目了然了。
静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。
另外,编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是“唯一的”,往往只能确定一个“更加合适的”版本,大家可以尝试运行一下以下程序,依次注释掉最匹配的结果,看看会打印出什么结果。
public class Overload {
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');
}
}
在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
我们来看一个动态分派的案例。
public class DynamicDispatch {
static abstract class Human {
protected abstract void sayHello();
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("hello, man");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("hello, woman");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
}
}
相信稍微了解一点Java的人都能回答出来,上面代码的运行结果是
hello, man
hello, woman
这是一个明显的Java重写方法案例,实际上,这个就是动态分派。只有在运行时,虚拟机才知道真正的执行主体是谁。
我们使用javap来分析一下上述代码的汇编指令
可以看到,最终都是要执行虚方法
invokevirtual #6 // Method cn/hewie/dispatch/dynamicdispatch/DynamicDispatch$Human.sayHello:()V
这里的sayHello方法已经在编译阶段确定了,但是其执行的主体是从"aload_1"、"aload_2"这样的变量中获取的,这里的值分别在上面的代码中设置为了Man和Women,所以产生这样的运行结果。动态分派里的“动态”就是这样体现出来的,这也是重写的在虚拟机层面的一种体现。
方法的接收者与方法的参数统称为方法的宗量。根据分派基于多少种宗量, 可以将分派划分为单分派和多分派两种。 单分派是根据一个宗量对目标方法进行选择, 多分派则是根据多个宗量对目标方法进行选择。
网上常说Java语言是一门静态多分派、 动态单分派的语言,具体来说是因为
参考文档:
深入理解Java虚拟机:JVM高级特性与最佳实践 周志明 著