方法调用
先来说说java方法的调用,方法的调用不等于方法执行,方法调用阶段唯一的任务是确定被调用方法的版本(即调用哪个方法,不是唯一的,确定一个“更加合适”的版本),不涉及方法内部的具体运行过程。
1 方法解析
“编译期可知,运行期不可变”的方法(静态方法和私有方法),在类加载的解析阶段,会将其符号引用转化为直接引用(入口地址)。这类方法的调用称为解析(Resolution)
我们都是知道java文件都需要编译成class文件,而一切方法调用在class文件里存储的都是符号引用,而不是方法的实际运行时内存布局的入口地址(相当于直接引用)。在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析成立的前提是:方法的程序真正运行之前就有一个可确认的调用版本,并且这个方法的调用版本在运行期是不可变的。换句话说,调用目标在程序代码写好、编辑器进行编译时就必须确认下来,这类方法调用的调用称为解析。
在Java虚拟机里提供了5条调用方法字节码指令,分别如下。
invokestatic:调用静态方法
invokespeciak: 调用实例构造器方法、私用方法和父类方法
invokevirtual: 调用所有的虚方法
invokeinterface:调用接口时,会在运行再确定一个实现接口的对象
invokedynamic:现在运行时动态解析出调用点限定符引用的方法,再执行方法
只有被invokestatic和invokespecial指令调用的方法,可以在解析阶段中确定调用的版本,符合这个条件的静态方法、私有方法、实例构造器、父类方法。它们在类加载的解析时候就会把符号引用解析为直接引用。这些方法被称为非虚方法。final虽然是使用invokevirtual来进行调用的,也是一个非虚方法
解析调用一定是一个静态的过程,在编译期间就完全确定,而分派调用可能是静态的也可能是动态的。
2.分派
静态分派最典型的应用就是方法重载。
在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。最典型的应用就是方法重写。
Java是一门面向对象的编程语言,因为Java具备面向对象的3个基本特征:封装、继承、多态。来看看虚拟机如何通过分派确定“重写”和”重载“方法的目标方法。
来看一个静态分配的例子
package com.jvm;
/**
* 静态分派
* @author renhj
*
*/
public class StaticDispatch {
static class Human {
}
static class Man extends Human {
}
static class Women extends Human {
}
public void sayHello(Human guy) {
System.out.println("hello, guy!");
}
public void sayHello(Man guy) {
System.out.println("hello, man!");
}
public void sayHello(Women guy) {
System.out.println("hello, women!");
}
public static void main(String[] args){
Human man = new Man();
Human women = new Women();
StaticDispatch sd = new StaticDispatch();
sd.sayHello(man);
sd.sayHello(women);
}
}
所有依赖静态类型来定位方法执行版本的分派动作称为静态分配,静态分配的典型动作是方法重载,静态分派发生在编译阶段,虽然编译器能确定方法的重载版本,但是很多情况下这个重载的版本并不是“唯一的”,往往只能确定一个“更加合适的”版本。产生这种模糊结论的主要原因是字面量不需要定义,所以字面量没有显示的静态类型,它的静态类型只能通过语言上的规则去理解和推断。
“更加合适”版本例子
package com.jvm;
/**
* 重载方法屁匹配优先级
* @author renhj
*
*/
public class Verload {
private static void sayHello(char arg){
System.out.println("hello char");
}
private static void sayHello(Object arg){
System.out.println("hello Object");
}
private static void sayHello(int arg){
System.out.println("hello int");
}
private static void sayHello(long arg){
System.out.println("hello long");
}
public static void main(String[] args) {
sayHello('c');
}
}
上面代码运行后,正常回输出:hello char,如果注释掉sayHello(char arg)方法,那输出就会变成:hello int。
3.动态分配
我们接下来看一下动态分配的过程,它和多态性的另外一个重要体现–重写(Override)有着密切的关系,先看例子。
package com.jvm;
/**
* 动态分派
* @author renhj
*
*/
public class DynamicDispatch {
static abstract class Human {
protected abstract void sayHello();
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("hello man!");
}
}
static class Women extends Human {
@Override
protected void sayHello() {
System.out.println("hello women!");
}
}
public static void main(String[] args){
Human man = new Man();
Human women = new Women();
man.sayHello();
women.sayHello();
man = new Women();
man.sayHello();
}
}
运行结果:
hello man!
hello women!
hello women!
这个结果相信不会出乎任何人的意料,那Java虚拟机是如何根据实际类型来分配方法执行版本的呢?我们使用javap命令输出这个类的字节码,尝试从中寻找答案,输出结果字节码如下。
0-15主要是建立man和woman的存储空间、调用Man和Woman类型的实例构造器,并将两个实例存放在第一个和第二个局部变量表Slot之中。接下来的16~21句是关键部分,16、20两句分别是把刚刚创建的两个对象的引用压到栈顶,这两个对象是方法是实际所有者,称为接受者。17、21两句是方法调用指令,这两条调用指令从字节角度来看,无论指令(invokevirtual)还是参数完全一样,但是这两条指令最终执行的目标方法并不相同,原因需要从invokevirtual指令的多态查找过程开始说起,invokevirtual指令的运行时解析过程大致如下几个步骤:
1). 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C.
2). 如果在类型C中找到与常量池中描述符和简单名称都相符的方法,则进行访问权限的校验,如果校验不通过,则返回java.lang.IllegaAccessError异常,校验通过则直接返回方法的直接引用,查找过程结束。
3). 否则,按照继承关系从下往上一次对C的各个父类进行第二步骤的搜索和验证过程。
4). 如果始终还是没有找到合适的方法直接引用,则抛出java.lang.AbstractMethodError异常。
由于invokevirtual指令执行的第一步是在运行时确定接收者的实际类型,所以两次中的invokevirtual指令把常量池中的类方法符号引用解析到不同的直接引用上,这个就是java语言中方法重写的本质,我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
总结:静态分派和动态分派以及所涉及的重载和重写其实可以拆分成两个步骤
1 动态分配:方法的实际所有者,也就是接受者如果会发生变化的话,就是根据实际过程中的用户指定,来选用具体的接受者(也就是实际类型),并且根据虚方法的调用规则,在实际类型中查找静态类型(也就是外观类型)的同描述和同方法名,如果没有找到,就会从其父类中查找,在这个过程中就会发生重写
2 静态分配:与之同时,在实际类型确认之后,在查找方法的同时,也会加入方法的入参类型作为判定条件,此时就是描述、方法名、入参类型的判定条件在确定的实际类型中查找方法,这是就是发生重载,同理,如果没有找到就在父类中查找。
那下面的复杂的例子中:就同时存在着静态分配和动态分派
public class OtherFunctionCLass {
void invoke(String s){
}
void invoke(SuperPerson s,String s2){
}
}
public class DispatchTest {
public void test(){
SuperPerson superPerson = new Person();
SuperPerson superPerson2 = new Person2();
OtherFunctionCLass otherFunctionCLass = new OtherFunctionCLass();
otherFunctionCLass.invoke(new Person().eat());
}
}
OtherFunctionCLass 的otherFunctionCLass变量 由于其指向的对象唯一,这里只是存在静态分派,根据入参类型选择了第一个重载方法
invoke(new Person().eat()) 这里存在了一个匿名对象,SuperPerson的变量可以指向new Person()和new Person2()两个对象,我们指定了new Person()这个对象,这时SuperPerson 的指向又明确了,这时根据虚方法的调用规则,会发生重写,在调用其eat() 方法时,根据入参类型选择其重载方法。
所有说:动态分派的关键是选用哪一个对象(在父子结构中就会出现重写),静态分派的关键是在一个对象中选择哪一个方法(重载)
4 当分派与多分派
参考这个:写的还行
java方法调用之单分派与多分派(二)
静态分派选择到目标方面:需要确定到类的哪个方法名一系列重载方法中的哪个,决定因素有方法名和入参类型两个,所以静态分派是多分派
动态分派:只是需要确定哪个类的方法,决定因素有一个(方法名)(确定到哪个方法有静态分派赋值),所以动态分派是单分派