关于方法调用的几个字节码指令:
invokestatic invokespecial invokevirtual invokeInterface invokedynamic
若您不太了解以上几个了解指令,这边引用别人写的一篇好文对比 5条方法调用字节码指令区别
JVM指令之invokestatic,invokespecial,invokeinterface,invokevirtual,invokedynamic
简单描述一下invokespecial和invokevirtual的区别:
1.invokespecial只能调用三类方法:包括实例初始化方法、私有方法和父类方法,指令用于调用一些需要特殊处理的实例方法。
2.invokevirtual是指令用于调用对象的实例方法,根据对象的实际类型进行分派。
下面开始我们的表演:
在阅读下文开始之前,先搞清楚一个概念。对于Animal an = new Bird();这句代码,我们把Animal叫做静态类型,Bird叫做实例类型。静态类型在编译期可知,实际类型在运行期才可以确定下来。
目录
Java方法调用主要分为
1、解析调用
2、静态分派调用(分静态单分派和静态多分派)
3、动态分派调用(分动态单分派和动态多分派)
方法调用不等同于方法的执行,方法的调用阶段的唯一任务就是确定被调用方法的版本。
解析:连接过程的最后一个阶段
解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程
所有方法调用中的目标方法在Class文件中都是一个常量池中的符号引用,在类加载的解析阶段,会将其中一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且运行期不可变。
能在编译之前确定被调用方法的版本的方法:
静态方法 //invokestatic
构造器方法 //invokespecial
私有方法 //invokespecial
父类方法 //invokespecial
以上方法对应invokestatic和invokespecial指令。解析调用是静态的过程,在编译期间就能确定。
静态分派最经典的案例是重载,重载根据参数的静态类型而不是实际类型作为判定依据。
而静态类型在编译期可知,所以静态分派发生在编译阶段。(静态类型与实际类型,目录处已经做相关说明)
来看一段代码:
package cn.itcats.jvm.invokemethod;
/**
* 静态分派调用
* @author fatah
*最经典的案例就是重载
*/
public class Demo {
static class Parent{
}
static class Child1 extends Parent {}
static class Child2 extends Parent{}
public void sayHello(Child1 child1) {
System.out.println("child1 is saying");
}
public void sayHello(Child2 child2) {
System.out.println("child2 is saying");
}
public void sayHello(Parent p) {
System.out.println("Parent is saying");
}
public static void main(String[] args) {
//多态 静态类型是Parent 实例类型是Child1
Parent p1 = new Child1();
Parent p2 = new Child2();
Demo d = new Demo();
//结果会是什么?
d.sayHello(p1);
d.sayHello(p2);
}
}
运行结果:
Parent is saying
Parent is saying
这就是静态分派的例子,阅读完上文后应该不难分析出结果,静态类型的确定在编译期间就已经确定了
改动一下main方法
public static void main(String[] args) {
//实例类型未改变,而真实类型发生了改变
Parent p = new Child1();
p = new Child2();
//那么静态类型可以发生改变吗?当然是可以的
Demo d = new Demo();
d.sayHello((Child2) p); //通过把p强制转换,把Parent类型的p转化为Child2类型的p
}
运行结果:
child2 is saying 由于静态类型的改变运行结果也发生了改变
总结上面的例子:方法的静态分派是由静态类型所确定,由于静态类型在编译期就可以确定,所以静态分派在编译期也可确定。
再来看一个例子:
package cn.itcats.jvm.invokemethod;
public class Demo2 {
public void sayHello(byte i) {
System.out.println("byte");
}
public void sayHello(short i) {
System.out.println("short");
}
public void sayHello(int i) {
System.out.println("int");
}
public void sayHello(int... i) {
System.out.println("int...");
}
public void sayHello(long i) {
System.out.println("long");
}
public static void main(String[] args) {
Demo2 d = new Demo2();
d.sayHello(2); //上面的方法好像看似都可以,到底哪个被调用呢?
}
}
运行结果: int
虽编译器能确定被调用方法的重载版本,但是在很多情况下重载版本并不唯一,往往只能确定一个更加合适的版本。
主要原因是字面量作为参数传入是没有显式的静态类型的。只能选择一个最贴近该字面型类型的方法。实际工作中要避免出现。
与静态分派不同,动态分派最经典的案例是重写,运行期根据方法接收者的实际类型来选择方法。
invokevirtual指令会优先寻找当前类中是否含有被重写的方法,如有直接调用本类方法,若没有找到,则在父类中寻找,直到找到为止并调用。
具体步骤: