方法调用
方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法)。
方法分为两种:
非虚方法:静态方法、私有方法、实例构造器、父类方法、被final修饰的方法
父类方法:子类中调用父类的方法;如super.method();
虽然final方法是使用invokevirtual指令来调用的,但是由于它无法被覆盖,没有其他版本,所以也无须对方法接收者进行多态选择,又或者说多态选择的结果肯定是唯一的
这部分方法,在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的(“编译期可知,运行期不可变”)。所有使用解析调用;
虚方法:被invokevirtual调用的所有方法(除了被final修饰的方法);使用分派来确定方法的调用。
1 解析调用
所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析(Resolution)。
解析调用一定是个静态的过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期再去完成。
2 分派
分派可能是静态的,也可能是动态的
- 静态类型与动态类型
public class Test {
static abstract class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
// 执行代码
public static void main(String[] args) {
Human man = new Man();
}
}
上面代码中:
Human称为变量的静态类型(Human:不会被改变、在编译器可知)
Man则称为变量的实际类型(Man:会变化、在运行期才可知)
2.1 静态分派
- 定义
根据 变量的静态类型 进行方法分派 的 行为
即根据 变量的静态类型 确定执行哪个方法
发生在编译期,所以不由 Java 虚拟机来执行
-
应用场景 方法重载(OverLoad)
-
实例
public class Test {
// 类定义
static abstract class Human {
}
// 继承自抽象类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();
Test test = new Test();
test.sayHello(man);
test.sayHello(woman);
}
}
// 运行结果
hello,guy!
hello,guy!
原因:
- 方法重载(
OverLoad) => 静态分派 => 根据 变量的静态类型 确定执行(重载)哪个方法 - 所以上述的方法执行时,是根据变量(
man、woman)的静态类型(Human)确定重载sayHello()中参数为Human guy的方法,即sayHello(Human guy)
变量的静态类型 发生变化 的情况
可通过 强制类型转换 改变 变量的静态类型
Human man = new Man();
test.sayHello((Man)man);
// 强制类型转换
// 此时man的静态类型从 Human 变为 Man
// 所以会调用sayHello()中参数为Man guy的方法,即sayHello(Man guy)
静态分派的优先级匹配问题
编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是“唯一的”,往往只能确定一个“更加合适的”版本。
2.2 动态分派
未完待续。。。