分派调用
Java
是一门面向对象的程序语言,因为Java
具备面向对象的3个基本特征
封装、继承、多态。分派调用将揭示多态的一些体现,如重载和重写。
静态分派
静态类型 & 动态类型
/**
* 静态分派
* 重载
*/
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
上边的代码中,Human
称为变量的“静态类型”(Static Type
),或者叫“外观类型”(Apparent Type
),
后面的Man
则被称为变量的“实际类型”(Actual Type
)或者叫“运行时类型”(Runtime Type
)。
静态类型和实际类型在程序中都可能发生变化,区别是静态类型的变化仅仅在使用时发生,变量本身
的静态类型不会被改变,并且最终的静态类型是在编译期可知的;
// 静态类型在使用时发生变化
sr.sayHello((Man) man); // hello gentleman
sr.sayHello((Woman) woman); // hello lady
而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。
执行结果的解释
虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判断依据的。由于静态类型在编译期可知,
所以在编译阶段javac
编译器就根据静态类型决定了会使用哪个重载版本。
所有依赖静态类型来决定方法执行版本的分派动作,都被称为静态分派。静态分派的最典型应用
表现就是方法重载。
重载的细节
需要注意javac
编译器虽然能确定方法的重载版本,但在很多情况下是确定一个“相对更合适”的版本。
产生这种模糊结论的主要原因是字面量天生的模糊性,它不需要定义,所以字面量就没有显示的静态类型,
它的静态类型只能通过语言、语法的规则去理解和判断。
/**
* 重载方法区匹配优先级
*/
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');
}
}
// 结果
hello char
如果注释了sayHello(char arg)
,将输出hello int
,这时发生了一次自动类型转换,char
-> int
。
如果注释了sayHello(int arg)
,将输出hello long
,这时发生两次自动类型转换,char
-> int
-> long
。
类型转换必须是安全的,上边的例子会按照
char
->int
->long
->float
->double
转换。
如果注释了sayHello(long arg)
,将输出hello Character
,这时发生一次自动装箱。
如果注释了sayHello(long Character
),将输出
hello Serializable,
Serializable是
Character`实现的一个接口。
如果类实现了多个接口,并且都提供了
sayHello(Interface arg)
,由于没有优先级,所以编译器无法确定是哪个接口,会提示“类型模糊”(
Type Ambiguous
)而拒绝编译。
如果注释了sayHello(long Serializable
),将输出hello Object
,char
装箱后转型为父类Object
。
如果有多个父类,那将按继承关系中从下往上开始搜索,越上层优先级越低。
如果注释了sayHello(long Object
),将输出hello char ...
,可变长参数的重载优先级是最低的,
a
字符被当作一个char[]
数组的元素。
动态分派
/**
* 动态分派
* 重写
*/
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();
}
}
// 结果
man say hello
Woman say hello
Woman say hello
执行结果解释
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 has $" + 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 has $" + money);
}
}
public static void main(String[] args) {
Father guy = new Son();
System.out.println("This guy has $" + guy.money);
}
}
// 结果
I am Son, i has $0
I am Son, i has $4
This guy has $2
创建Son
对象时,首先隐式调用了Father
的构造函数,而Father
的构造函数中对
showMeTheMoney()
的调用是一次虚方法调用,执行的是实际类型的实现,也就Son
的实现,
此时Son
的money还没有初始化,所以输出I am Son, i has $0
。
之后就是Son
自己的构造函数,输出I am Son, i has $4
。
最后一句通过静态类型(Father
)访问到了父类中的money
,输出了2。(字段不参与多态)。
单分派与多分派
方法的接收者与方法的参数统称为方法的宗量,根据分派基于多少种宗量可分为单分派和多分派。
单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。
/**
* 单分派、多分派演示
*/
public class Dispatch {
static class QQ{ }
static class _360{ }
public static class Father{
public void hardChoice(QQ arg) {
System.out.println("father choose qq");
}
public void hardChoice(_360 arg) {
System.out.println("father choose 360");
}
}
public static class Son extends Father {
public void hardChoice(QQ arg) {
System.out.println("son choose qq");
}
public void hardChoice(_360 arg) {
System.out.println("son choose 360");
}
}
public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.hardChoice(new _360());
son.hardChoice(new QQ());
}
}
// 结果
father choose 360
son choose qq
编译阶段中编译器的选择过程就是静态分派的过程。这时候选择目标方法的依据有两点:
- 静态类型是
Father
还是Son
- 方法参数是
QQ
还是_360
这次的选择就过的最终产物是产生了两条invokevirtual
指令,这两条指令的参数分别
为常量池中指向Father::hardChoice(_360)
以及Father::hardChoice(QQ)
方法的符号引用。
因为是根据两个宗量进行选择,所以Java
语言的静态分派属于多分派类型(静态类型+方法参数)。
运行阶段虚拟机的选择,也就是动态分派的过程。在执行invokevirtual
指令时,虚拟机不会
关心传递过来的参数是什么,因为这时候参数是静态类型还是实际类型都对方法的选择不构成
任何影响,唯一可以影响虚拟机选择的因素只有该方法的接收者的实例类型是Father
还是Son
。
因为只有一个宗量作为选择依据,所以Java
语言的动态分派属于单分派类型。
总结
Java
语言是一门静态多分派、动态单分派的语言。