方法调用
方法调用不等同与方法执行,方法调用阶段唯一的任务就是确定调用方法的版本。就是调用哪一个方法。不涉及到方法内部运行过程。
解析
方法调用的目标方法在Class文件里面都是一个常量池中的符号引用。
在类加载解析阶段,会将一部分符号引用转化为直接引用,这种解析成立的前提就是:方法在程序执行之前就有一个可确定的调用版本,并且这个调用版本在运行期间是不可以改变的。这类方法调用称为“解析”(Resolution)。
符合上面描述的方法主要是:静态方法,私有方法。
静态方法与类型直接关联,
私有方法外部不可访问。
因此他们都适合在类加载阶段进行解析。
Java虚拟机方法调用字节码指令为:
- invokestatic:调用静态方法
- invokespecial:调用实例构造器<init>方法、私有方法和父类方法
- invokevirtual:调用所有的虚方法
- invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象
- invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,前4条指令,分派逻辑是固化在Java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
只要能被 invokestatic 和 invokespecial 指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合条件的有 静态方法、私有方法、实例构造器、父类方法 4类,他们在类加载的时候会把符号引用解析为该方法的直接引用,称为“非虚方法”。其他的方法称为“虚方法”(除了final方法)
public class StaticMethod {
//类方法
public static void test(){
System.out.println("test");
}
public static void main(String[] args) {
StaticMethod.test();
}
}
虽然 final 方法是使用 invokevirtual 指令来调用的,但是由于它无法被覆盖,没有其他版本,所以也无须对方法接收者进行多态选择,或者说选择的结果肯定是唯一的,Java规范中也明确说明了 final 方法是一种非虚方法。
解析调用一定是个静态的过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用全部变为直接引用,不会在运行期间完成。而分派调用可能是静态的,也可能是动态的,可以分为4中,分别是 静态单分派、静态多分派、动态单分派、动态多分派。
分派
Java是面向对象的语言,具备3个基本特征:继承、封装、多态。
1.静态分派
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 Human");
}
public void sayHello(Man guy){
System.out.println("hello Man");
}
public void sayHello(Woman guy){
System.out.println("hello Woman");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch staticDispatch = new StaticDispatch();
staticDispatch.sayHello(man);
staticDispatch.sayHello(woman);
}
}
执行结果
hello Human
hello Human
Process finished with exit code 0
Human man = new Man();
上面代码中的“Human”称为变量的静态类型(Static Type),或者叫做变量的外观类型(Apparent Type),后面的“Man”则称为变量的实际类型(Actual Type),静态类型和实际类型在程序中都可以发生变化。区别是:
- 静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期间可知的;
- 实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么
public static void main(String[] var0) {
StaticDispatch.Man var1 = new StaticDispatch.Man();
StaticDispatch.Woman var2 = new StaticDispatch.Woman();
StaticDispatch var3 = new StaticDispatch();
var3.sayHello((StaticDispatch.Human)var1);
var3.sayHello((StaticDispatch.Human)var2);
}
通过反编译class文件我们看到,实际编译器在编译期间已经把变量变化为“Human”,如果我们想要让静态类型变化,也只能强制转换类型或者在创建对象时就指定实际类型。
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch staticDispatch = new StaticDispatch();
staticDispatch.sayHello((Man) man);
staticDispatch.sayHello((Woman) woman);
}
public static void main(String[] args) {
Man man = new Man();
Woman woman = new Woman();
StaticDispatch staticDispatch = new StaticDispatch();
staticDispatch.sayHello(man);
staticDispatch.sayHello(woman);
}
hello Man
hello Woman
Process finished with exit code 0
在方法接收者已经确定是对象“staticDispatch”的前提下,使用那个重载版本,就完全取决于传入参数的数量和数据类型。静态类型是编译器可知的,Javac编译器会根据参数的静态类型决定使用哪个重载版本。
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载,静态分派发生在编译阶段。
重载方法匹配优先级
/**
* 静态分派,方法重载
*/
public class StaticOverLoad {
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) {
StaticOverLoad.sayHello('x');
}
}
代码输出 hello char
这个很好理解 ‘x’是个char类型的数据,如果注释掉sayHello(Object arg),那么输入变成了 hello int
这时发生了一次自动类型转换,‘x’除了可以表示一个字符串,也可以表示数字120,因此参数类型为int的重载也是合适的,如果继续注释掉sayHello(int arg)方法,那么输出变成了
hello long
这里发生了两次自动类型转换,‘x’转型为整数120之后,又转变为了120L,实际自动类型转换能继续发生多次,按照 char -> int -> logn -> float -> double 的顺序转型进行匹配。但是不会匹配到 byte和short类型的重载,因为char到byte或short的转型是不安全的。
为什么不安全
The range of the char type is 0 to 2^16 - 1 (0 to 65535).
The short range is -2^15 to 2^15 - 1 (−32,768 to 32,767).
因为char类型的范围是 0 到 2的16次方减1,就是0-65535
而short类型的范围则是 −32,768 to 32,767
如果高位char类型转型成short会有丢失精度的问题。
继续注释掉sayHello(loan arg),那输出会变为
hello Character
这时是发生了一次自动装箱,‘x’被包装为它的封装类型 java.lang.Character,所以匹配到了参数类型为Character的重载,继续注释掉sayHello(Character arg),那么输出会变为:
hello Serializable
输出Serializable,是因为java.lang.Serializable是java.lang.Character类实现的一个接口,当自动装箱之后发现还是找不到装箱类,但是找到了装箱类实现了的接口类型,所以紧接着又发生一次自动转型,char可以转型成int,但是Character是绝对不会转型为Integer的,它只能安全的转型为它的实现的接口或父类。Character还实现了另外一个接口java.lang.Comparable<Character>,如果同时出现两个参数分别为Serializable和Comparable<Character>的重载方法,那么他们此时的优先级是一样的,编译器无法确定要自动转型为哪种类型,拒绝编译,程序必须显示的指定字面量的静态类型,如:
sayHello((Comparable<Character>)'x')
才能编译通过。
继续注释掉 sayHello(Serializable arg),输出将会变为
hello object
这时是char装箱后转型为父类,如果有多个父类,那将在继承关系中从下王上开始搜索,越接近上层的优先级越低。即使方法调用传入的参数值为null时,这个规则仍然适用。如果我们把sayHello(object arg)也注释掉,输出将会变为
hello char. . .
7个重载方法已经被注释的只剩一个了,可见变长参数的重载优先级最低。
2.动态分派
动态分派,是多态性的另外一个重要体现“重写”有着密切的关系。
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
Process finished with exit code 0
通过javap命令输出字节码
17: invokevirtual #6 // Method com/pintec/javaDynamic/DynamicDispatch$Human.sayHello:()V
20: aload_2
21: invokevirtual #6 // Method com/pintec/javaDynamic/DynamicDispatch$Human.sayHello:()V
24: new #4 // class com/pintec/javaDynamic/DynamicDispatch$Woman
27: dup
28: invokespecial #5 // Method com/pintec/javaDynamic/DynamicDispatch$Woman."<init>":()V
31: astore_1
32: aload_1
33: invokevirtual #6 // Method com/pintec/javaDynamic/DynamicDispatch$Human.sayHello:()V
可以看到调用sayHello方法的指令都是invokevirtual。
invokevirtual指令大致可以分为一下几个步骤:
- 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C
- 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限的校验,如果通过则返回这个方法的直接引用,查找结束,如果不通过抛错。
- 否则,按照继承关系从下网上一次对C的父类进行第二步的搜索和验证过程。
- 如果始终没有找到合适的方法,抛错。
由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个就是Java语言中方法重写的本质。
3.单分派与多分派
public class Dispatch {
static class QQ{}
static class _360{}
public static class Father{
public void choice(QQ qq){
System.out.println("father choice qq");
}
public void choice(_360 qq){
System.out.println("father choice _360");
}
}
public static class Son extends Father{
@Override
public void choice(QQ qq){
System.out.println("Son choice qq");
}
@Override
public void choice(_360 qq){
System.out.println("Son choice _360");
}
}
public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.choice(new _360());
son.choice(new QQ());
}
}
father choice _360
Son choice qq
Process finished with exit code 0

先看编译阶段的选择过程,也就是静态分派的过程,这时选择目标方法的依据有两点:
- 静态类型是Father还是Son
- 方法参数是QQ还是360
这次选择结果最终是产生了量条invokevirtual指令,两条指令的参数分别为常量池中指向
Father.choice(360)和Father.choice(qq)方法的符号引用,因为根据连个宗量进行选择,所以java语言的静态分派属于多分派类型。
在看运行阶段虚拟机的选择,也就是动态分派的过程,在执行son.choice(new QQ())这句代码时,更准确地说,是在执行这句代码所对应的invokevirtual指令时,由于编译器已经决定目标方法的签名必须为choice(QQ),此时不关心传递过来的参数是什么,因为参数是静态类型、实际类型都对方法的选择不会构成任何影响,唯一可以影响虚拟机选择的因素只有此方法的接收者的实际类型是Father还是Son。因为只有一个宗量作为选择依据,所以Java语言的动态分派属于单分派类型。