更新时间:2020-04-23 14:36:27 来源:极悦 浏览2021次
方法调用并不等同于方法中的代码被执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还未涉及方法内部的具体运行过程。一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(也就是之前说的直接引用)。
解析
所有方法调用的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能够成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。这类方法的调用被称为解析(Resolution),在Java语言中符合这种要求的主要有静态方法和私有方法。
方法调用指令
invokestatic:用于调用静态方法。
invokespecial:用于调用实例构造器<init>()方法、私有方法和父类中的方法。
invokevirtual:用于调用所有的虚方法。
invokeinterface:用于调用接口方法,会在运行时再确定一个实现该接口的对象。
invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。
前面4条调用指令,分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户设定的引导方法来决定的。
方法分类
在java语言中方法主要分为“虚方法”和“非虚方法”。
非虚方法:在类加载的时候就可以把符号引用解析为该方法的直接引用。比如:静态方法、私有方法、实例构造器、父类方法和被final修饰的方法。
虚方法:需要在运行时才能将符号引用转换成直接引用,如,分派。
分派
分派(Dispatch)它可能是静态的也可能是动态的,按照分派依据的宗量数可分为单分派和多分派。这两类分派方式两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派4种分派组合情况。
静态分派
依赖静态类型来决定方法执行版本的分派动作,都称为静态分派。静态分派的最典型应用表现就是方法重载,虚拟机(或者准确地说是编译器)在重载时是通过参数的静态类型来作为判定依据的。
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)或者叫“运行时类型”(Runtime Type)。
动态分派
我们把在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。最典型的表现就是重写。
public class DynamicDispatch{
static abstract class Human{
abstract void sayHello();
}
static class Man extends Human{
public void sayHello(){
System.out.println("hello,Man!");
}
}
static class Woman extends Human{
public void sayHello(){
System.out.println("hello,Woman!");
}
}
public static void main(String[]args){
Human man=new Man();
Human woman=new Woman();
man.sayHello();
woman.sayHello();
}
}
运行结果:
hello,Man!
hello,Woman!
我们通过javap命令看下main方法的字节码:
...
public static void main(java.lang.String[]);
descriptor:([Ljava/lang/String;)V
flags:ACC_PUBLIC,ACC_STATIC
Code:
stack=2,locals=3,args_size=1
0:new
3:dup
4:invokespecial
7:astore_1
8:new
12:invokespecial
15:astore_2
16:aload_1
17:invokevirtual
20:aload_2
21:invokevirtual
24:return
LineNumberTable:
line 27:0
line 28:8
line 29:16
line 30:20
line 31:24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 args[Ljava/lang/String;
8 17 1 man Lcom/xiaolyuh/DynamicDispatch$Human;
16 9 2 woman Lcom/xiaolyuh/DynamicDispatch$Human;
}
...
通过字节码我们发现:在main方法中,sayHello()方法的调用对应的符号引用是一样的,com/xiaolyuh/DynamicDispatch$Human.sayHello:()V。在这里我们可以得出一个结论:在动态分派的情况下,在编译时期我们是无法确定方法的直接引用的,那么它是怎么实现重载方法的调用的呢?问题关键是在invokevirtual指令上,在执行invokevirtual指令时,invokevirtual指令会去确定方法的调用版本。
以上就是极悦java培训机构的小编针对“Java基础学习:java方法调用”的内容进行的回答,希望对大家有所帮助,如有疑问,请在线咨询,有专业老师随时为你服务。
0基础 0学费 15天面授
Java就业班有基础 直达就业
业余时间 高薪转行
Java在职加薪班工作1~3年,加薪神器
工作3~5年,晋升架构
提交申请后,顾问老师会电话与您沟通安排学习