方法调用不等同于方法代码执行,解析阶段部分符号引用转化为直接引用
方法的使用并不直接导致其中代码的执行,在方法调用的过程中,其主要职责是识别并选定具体要执行的方法版本,而这个过程并不包括对方法内部执行细节的处理。在Class文件中,存储的仅是方法的符号引用,而非方法运行时在内存中的实际入口地址,即我们之前所提到的直接引用。

解析
在Class文件中,所有方法调用的目标方法均对应常量池中的一个符号引用。在类加载过程中的解析阶段,部分符号引用会被转换成直接引用。然而,这种转换能够实现,必须满足以下条件:方法在程序实际运行前就确定了其调用版本,并且这个版本在运行期间保持不变。换言之,一旦程序代码编写完成并经过编译器编译,目标调用便已固定。此类调用过程称作解析,而在Java编程语言中,符合这一特性的主要是静态方法和私有方法。
方法调用指令
invokestatic:用于调用静态方法。
用于启动实例构建器函数、私密函数以及继承自父类的方法。
invokevirtual:用于调用所有的虚方法。
invokeinterface:此功能旨在执行接口方法的调用,且在程序运行过程中,将最终确定一个具体实现了该接口的对象。
invokedynamic机制首先在运行阶段解析出调用点限定符所指向的方法,随后便执行该方法。
前四条指令的分配机制已固定于Java虚拟机之中,至于invokedynamic指令的分配机制,则是由用户自定义的引导方法所决定的。
方法分类
在java语言中方法主要分为“虚方法”和“非虚方法”。
非虚方法的特点在于,它们能够在类被加载阶段就将符号引用转化为直接引用。这包括静态方法、私有方法、实例构造器、父类中的方法以及被final关键字修饰的方法。
虚方法在执行过程中,能够将符号引用转变为直接引用,例如进行分派操作。
分派
分派机制可以是静态的,亦或是动态的;依据分派所依据的宗量数,它可以被划分为单分派和多元分派两种类型。将这两种分派方式相互结合,便形成了四种不同的分派组合:静态单分派、静态多分派、动态单分派以及动态多分派。
静态分派
静态类型被用来确定方法执行的具体版本,此类分派行为统称为静态分派。其中,方法重载是静态分派最典型的应用场景。在重载过程中,虚拟机(亦或更确切地说是编译器)会依据参数的静态类型来进行判断。
public class StaticDispatch{
static abstract class Human{
}
定义了一个静态类,名为Man,它继承自Human类。
}
定义了一个静态类,其名称为Woman,该类继承自Human类。
}
public方法sayHello接受一个Human类型的参数guy{
控制台输出:“你好,朋友!”
}
public方法sayHello接受一个Man类型的参数guy{
System.out输出文本"hello,绅士!"。
}
public 方法名为 sayHello,参数为 Woman 类型的 guy。
输出文本:“您好,女士!”
}
Human man=new Man();
Human woman=new Woman();
创建了一个名为StaticDispatch的对象,并对其进行初始化,赋值给sr变量。
sr.sayHello(man);
sr.sayHello(woman);
}
}
运行结果:
hello,guy!
hello,guy!
Human man=new Man();
此处所说的Human代表的是变量的“静态类型”,亦即“外观类型”;而Man则指的是变量的“实际类型”,亦或是“运行时类型”。
动态分派
我们将基于运行期间具体类型来决定执行版本的分派过程称作动态分派,而这一过程最显著的特征便是重写行为。
public class DynamicDispatch{
static abstract class Human{
abstract void sayHello();
}
static class Man extends Human{
public void sayHello(){
输出:"hello,Man!"至控制台。
}
}
static class Woman extends Human{
public void sayHello(){
输出到控制台的内容为:“您好,女士!”
}
}
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 执行主函数,参数为java.lang.String数组类型;
描述符:以字符串数组形式表示,用于方法调用。
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:
启动长度槽位名称签名
不得对参数数组进行修改。
在编号8的文档中,第17页,有一条关于名为Lcom/xiaolyuh/DynamicDispatch$Human的男性的记录。
禁止对编号为16、9、2的女性,即来自Lcom/xiaolyuh/DynamicDispatch$Human类的对象进行修改。
}
...
从字节码分析来看,我们可以观察到:在main方法内部,对于sayHello()方法的调用,所对应的符号引用均保持一致,具体表现为com/xiaolyuh/DynamicDispatch$Human.sayHello:()V。在此,我们能够提炼出一个观点:在动态分配的场景中,编译阶段我们无法预知具体方法的直接引用,那么它是如何完成方法重载的调用呢?核心在于invokevirtual指令,当执行此指令时,它会负责识别出正确的方法调用版本。
此内容为动力节点Java培训机构的编辑就“Java基础学习中的方法调用”主题所提供的解答,期望它能对各位读者有所裨益。若在阅读过程中有任何疑问,欢迎通过在线方式咨询,我们将有经验丰富的教师为您提供及时的帮助。