详解java虚拟机方法调用

方法调用

方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。

所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。 换句话说,调用目标在程序代码写好、 编译器进行编译时就必须确定下来。 这类方法的调用称为解析(Resolution)。

在Java语言中符合“编译期可知,运行期不可变”这个要求的方法,主要包括静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法各自的特点决定了它们都不可能通过继承或别的方式重写其他版本,因此它们都适合在类加载阶段进行解析。

与之相对应的是,在Java虚拟机里面提供了5条方法调用字节码指令,分别如下。

invokestatic:调用静态方法。
invokespecial:调用实例构造器<init>方法、 私有方法和父类方法。
invokevirtual:调用所有的虚方法。
invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在Java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、 私有方法、 实例构造器、 父类方法4类,它们在类加载的时候就会把符号引用解析为该方法的直接引用。

这些方法可以称为非虚方法,与之相反,其他方法称为虚方法(除去final方法)。

代码清单8-5 方法静态解析演示

/**
*方法静态解析演示
**
@author zzm
*/
public class StaticResolution{
public static void sayHello(){
System.out.println("hello world");
}
public static void main(String[]args){
StaticResolution.sayHello();
}}

使用javap命令查看这段程序的字节码,会发现的确是通过invokestatic命令来调用sayHello()方法的。

D:\Develop\>javap-verbose StaticResolution
public static void main(java.lang.String[]);
Code:
Stack=0,Locals=1,Args_size=1
0:invokestatic#31;//Method sayHello:()V
3return
LineNumberTable:
line 150
line 163

Java中的非虚方法除了使用invokestatic、 invokespecial调用的方法之外还有一种,就是被final修饰的方法。 虽然final方法是使用invokevirtual指令来调用的,但是由于它无法被覆盖,没有其他版本,所以也无须对方法接收者进行多态选择,又或者说多态选择的结果肯定是唯一的。 在Java语言规范中明确说明了final方法是一种非虚方法。

解析调用一定是个静态的过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期再去完成。 而分派(Dispatch)调用则可能是静态的也可能是动态的,根据分派依据的宗量数可分为单分派和多分派。 这两类分派方式的两两组合就构成了静态单分派、 静态多分派、 动态单分派、 动态多分派4种分派组合情况

分派

1.静态分派
代码清单8-6 方法静态分派演示

package org.fenixsoft.polymorphic;
/**
*方法静态分派演示
*@author zzm
*/
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 man=new Man();

我们把上面代码中的“Human”称为变量的静态类型(Static Type),或者叫做的外观类型(Apparent Type),后面的“Man”则称为变量的实际类型(Actual Type),静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。 例如下面的代码:

//实际类型变化
Human man=new Man();
man=new Woman();
//静态类型变化
sr.sayHello((Man)man)
sr.sayHello((Woman)man)

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。 静态分派的典型应用是方法重载。静态分派发生在编译阶段.

编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是“唯一的”,往往只能确定一个“更加合适的”版本。 这种模糊的结论在由0和1构成的计算机世界中算是比较“稀罕”的事情,产生这种模糊结论的主要原因是字面量不需要定义,所以字面量没有显式的静态类型,它的静态类型只能通过语言上的规则去理解和推断。

代码清单8-7 重载方法匹配优先级

package org.fenixsoft.polymorphic;
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

这很好理解,’a’是一个char类型的数据,自然会寻找参数类型为char的重载方法,如果注释掉sayHello(char arg)方法,那输出会变为:

hello int

这时发生了一次自动类型转换,’a’除了可以代表一个字符串,还可以代表数字97(字符’a’的Unicode数值为十进制数字97),因此参数类型为int的重载也是合适的。 我们继续注释掉sayHello(int arg)方法,那输出会变为:

hello long

这时发生了两次自动类型转换,’a’转型为整数97之后,进一步转型为长整数97L,匹配了参数类型为long的重载。

不过实际上自动转型还能继续发生多次,按照char->int->long->float->double的顺序转型进行匹配。 但不会匹配到byte和short类型的重载,因为char到byte或short的转型是不安全的。

继续注释掉sayHello(long arg)方法,那输出会变为:

hello Character

这时发生了一次自动装箱,’a’被包装为它的封装类型java.lang.Character,所以匹配到了参数类型为Character的重载,继续注释掉sayHello(Character arg)方法,那输出会变为:

hello Serializable

出现hello Serializable,是因为java.lang.Serializable是java.lang.Character类实现的一个接口,当自动装箱之后发现还是找不到装箱类,但是找到了装箱类实现了的接口类型,所以紧接着又发生一次自动转型。

char可以转型成int,但是Character是绝对不会转型为Integer的,它只能安全地转型为它实现的接口或父类。 Character还实现了另外一个接口java.lang.Comparable<Character>,如果同时出现两个参数分别为Serializable和Comparable<Character>的重载方法,那它们在此时的优先级是一样的。

程序必须在调用时显式地指定字面量的静态类型,如:sayHello((Comparable<Character>)’a’),才能编译通过。 下面继续注释掉sayHello(Serializable arg)方法,输出会变为:

hello Object

这时是char装箱后转型为父类了,如果有多个父类,那将在继承关系中从下往上开始搜索,越接近上层的优先级越低。 即使方法调用传入的参数值为null时,这个规则仍然适用。我们把sayHello(Object arg)也注释掉,输出将会变为:

hello char……

解析与分派这两者之间的关系并不是二选一的排他关系,它们是在不同层次上去筛选、 确定目标方法的过程。 例如,前面说过,静态方法会在类加载期就进行解析,而静态方法显然也是可以拥有重载版本的,选择重载版本的过程也是通过静态分派完成的。

猜你喜欢

转载自blog.csdn.net/u012124438/article/details/79342009