Java 是一门面向对象的程序语言,因为Java 具备面向对象的3 个基本特征:继承、封装和多态。这三个特征并不是各自独立的,从一定角度上看,封装和继承几乎都是为多态而准备的,多态的体现主要表现在方法的调用上,而方法在调用时会根据你送入的参数有不同的表现形式,这个就是分派:
1.编译期根据对象的静态类型进行静态分派。
2.运行期根据对象的实际类型进行动态分派。
那么我们都知道,方法的调用主要体现在对方法的重载和重写上,那么其实这里,静态分派对应的就是方法的重载,动态分派对应的是方法的调用。
静态分派
静态分派的最直接的解释是在重载的时候是通过参数的静态类型而不是实际类型作为判断依据的。
static abstract class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
public void sayHello(Human guy) {
System.out.println("你好,我是一个人!");
}
public void sayHello(Man guy) {
System.out.println("你好,我是一个男人!");
}
public void sayHello(Woman guy) {
System.out.println("你好,我是一个女人!");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
Demo demo = new Demo();
demo.sayHello(man);
demo.sayHello(woman);
}
result:
你好,我是一个人!
你好,我是一个人!
- 方法重载(
OverLoad
) = 静态分派 = 根据 变量的静态类型 确定执行(重载)哪个方法 - 所以上述的方法执行时,是根据变量(
man
、woman
)的静态类型(Human
)确定重载sayHello()
中参数为Human guy
的方法,即sayHello(Human guy)。
- “Human”称为变量的静态类型(Static Type),或者叫做的外观类型(Apparent Type),后面的“Man”则称为变量的实际类型(Actual Type)。
- 静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。
动态分派
动态分派的最直接的解释是在重写的时候是通过参数实际类型作为判断依据的。
class Human {
public void sayHello(){
System.out.println("Human say hello");
}
}
class Man extends Human {
@Override
public void sayHello() {
System.out.println("man say hello");
}
}
class Woman extends Human {
@Override
public void sayHello() {
System.out.println("woman say hello");
}
}
public static void main(String[] args) {
Human man = new Man();
man.sayHello();
man = new Woman();
man.sayHello();
}
运行结果:
man say hello
woman say hello
重写使用的是invokevirtual 指令,只是这个时候具备多态性。
invokevirtual 指令有多态查找的机制,该指令运行时,解析过程如下:
- 找到操作数栈顶的第一个元素所指向的对象实际类型,记做c;
- 如果在类型c 中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过将 常量池中 类方法符号引用 解析到不同的直接引用上,否则,按照继承关系从下往上依次对c 的各个父类进行第二步的搜索和验证过程,这就是Java 语言中方法重写的本质。
这块也涉及到了操作数栈中对于动态链接的定义:在方法调用的时候。部分符号引用在运行期间转化为直接引用,这种转化就是动态链接。
方法表
动态分派会执行非常频繁的动作,JVM 运行时会频繁的、反复的去搜索元数据,所以JVM 使用了一种优化手段,这个就是在方法区中建立一个虚方法表。使用虚方法表索引来替代元数据查找以提高性能。空间换时间的思想
在实现上,最常用的手段就是为类在方法区中建立一个虚方法表。虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。
PPT 图中,Son 重写了来自Father 的全部方法,因此Son 的方法表没有指向Father 类型数据的箭头。但是Son 和Father都没有重写来自Object 的方法,所以它们的方法表中所有从Object 继承来的方法都指向了Object 的数据类型。