【Java面试重点】Java方法调用——解析与分派

关于方法调用的几个字节码指令:

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、动态分派调用(分动态单分派和动态多分派)


 

Java方法调用主要分为

1、解析调用

方法调用不等同于方法的执行,方法的调用阶段的唯一任务就是确定被调用方法的版本。

 

解析:连接过程的最后一个阶段

解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程

 

所有方法调用中的目标方法在Class文件中都是一个常量池中的符号引用,在类加载的解析阶段,会将其中一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且运行期不可变。

能在编译之前确定被调用方法的版本的方法:

              静态方法                        //invokestatic

              构造器方法                   //invokespecial

              私有方法                       //invokespecial

              父类方法                       //invokespecial

以上方法对应invokestatic和invokespecial指令。解析调用是静态的过程,在编译期间就能确定。

 

2、静态分派调用(分静态单分派和静态多分派)

静态分派最经典的案例是重载,重载根据参数的静态类型而不是实际类型作为判定依据。

而静态类型在编译期可知,所以静态分派发生在编译阶段。(静态类型与实际类型,目录处已经做相关说明)

来看一段代码:

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

 

虽编译器能确定被调用方法的重载版本,但是在很多情况下重载版本并不唯一,往往只能确定一个更加合适的版本。

主要原因是字面量作为参数传入是没有显式的静态类型的。只能选择一个最贴近该字面型类型的方法。实际工作中要避免出现。

 

3、动态分派调用(分动态单分派和动态多分派)

与静态分派不同,动态分派最经典的案例是重写,运行期根据方法接收者的实际类型来选择方法。

invokevirtual指令会优先寻找当前类中是否含有被重写的方法,如有直接调用本类方法,若没有找到,则在父类中寻找,直到找到为止并调用。

    具体步骤:

  • 找到操作数栈顶的第一个元素所指向的对象的实际类型
  • 如果在实际类型中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则直接返回这个方法的直接引用,查找过程结束,如果不通过,抛出异常。
  • 按照继承关系从下往上依次对实际类型的各父类进行搜索与验证
  • 如果始终没有找到,则抛出AbstractMethodError

 


 

 

 

你可能感兴趣的:(Java虚拟机)