从 JVM 的视角看 Java 之方法调用(二):分派(涉及 Java 中的重载与重写)

从 JVM 的视角看 Java 之方法调用(一)一文中,我们知道 Java 虚拟机(JVM,Java Virtual Machine)总共支持5条方法调用的字节码指令,分别是 invokestatic、invokespecial、invokevirtual、invokeinterface 和 invokedynamic

对于可以被前两条指令调用的方法(除此之外还包含被 final 修饰的方法,尽管它由 invokevirtual 指令调用),由于其符合“编译期可知,运行期不可变”的要求,在类加载的时候就可以把符号引用解析为该方法的直接引用。而 invokedynamic 则是 JDK 1.7 为了更好地支持动态类型语言而引入的指令。

解析调用一定是个静态的过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号引用全部转变为明确的直接引用,不必延迟到运行期再去完成。而另一种主要的方法调用形式:分派(Dispatch)调用则要复杂许多,它可能是“静态的”也可能是“动态的”

分派调用过程将会揭示 Java 多态性特征的一些最基本的体现,例如“重载”和“重写”。

1. 静态类型和实际类型

为更好的理解下面的内容,这里先给出两个概念:

	Human man = new Man();
静态类型静态类型在编译期是可知的,它也被叫做“外观类型”,即变量本身的类型。在上面的例子中,变量 man 的静态类型为 Human。
实际类型实际类型在运行期才可确定,它指的是变量所指向对象的类型。在上述例子中,变量 man 的实际类型为 Man。

2. 静态分派

注:静态分派在《Java虚拟机规范》和《Java语言规范》里的说法都是“Method Overload Resolution”,即应该归入“解析”中。但部分其他外文资料和国内翻译的许多中文资料都将这种行为称为“静态分派”,在本文中也选取了这种说法。


首先,先来看一段关于静态分派和重载的代码:

public class StaticDispatch {
	static abstract class Human { }
	
	static class Man extends Human { }
	
	static class Woman extends Human { }
	
	public void say(Human human) {
		System.out.println("is human!");
	}
	
	public void say(Man man) {
		System.out.println("is man!");
	}
	
	public void say(Woman woman) {
		System.out.println("is woman!");
	}
	
	public static void main(String[] args) {
		Human man = new Man();
		Human woman = new Woman();
		StaticDispatch sd = new StaticDispatch();
		sd.say(man);
		sd.say(woman);
		Man man1 = new Man();
		Woman woman1 = new Woman();
		sd.say(man1);
		sd.say(woman1);
	}
}

如果对于 Java 的方法重载比较熟悉的话,应该不难给出这段代码的运行结果:

is human!
is human!
is man!
is woman!

这段代码就是静态分派的一个典型示例,对于静态分派,大家可以这么理解:

所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派。

说到这里,上述代码为什么是这个执行结果?大家想必都有了自己的猜想。没错,Java 中的方法重载就属于静态分派!

JVM(或者准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。由于静态类型在编译期可知,所以在编译阶段,Javac 编译器就会根据参数的静态类型,决定使用哪个重载版本,并把这个方法的符号引用写到相应的 invokevirtual 指令的参数中去。因此,在前两次调用 say 方法时,由于传入变量的静态类型均为 Human,所以都打印了“is human!”。而在后两次调用 say 方法时,传入变量的类型变成了 Man 和 Woman,故打印的内容也发生了改变。

由于静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的,这点也是为何一些资料选择把它归入“解析”而不是“分派”的原因。最后,我们可以利用 javap 工具,验证下 say 方法是不是由 invokevirtual 指令调用的:

验证invokevirtual指令

3. 动态分派

重载通过静态分派实现,而重写则是通过动态分派实现的。与静态分派类似,对于动态分派,大家可以这么理解:

所有依赖实际类型来决定方法执行版本的分派动作,都称为动态分派。

类似的,再来看一段关于重写的代码:

public class StaticDispatch {
	static interface Human {
		public abstract void say();
	}
	
	static class Man implements Human {
		@Override
		public void say() {
			System.out.println("is man!");
		}
	}
	
	static class Woman implements Human {
		@Override
		public void say() {
			System.out.println("is woman!");
		}
	}
	
	public static void main(String[] args) {
		Human man = new Man();
		Human woman = new Woman();
		man.say();
		woman.say();
	}
}

由于前面已经给出了 invokevirtual 指令的相关例子,在这里将原本的抽象类 Human 修改为了接口,方便最后利用 javap 工具验证 invokeinterface 指令。相信对于这段代码,大家应该可以很快的给出程序输出的内容,毕竟 Java 是一门面向对象的语言,而重写是构成 Java 多态的十分重要的一部分,在日常的编程当中也十分的常见。

is man!
is woman!

显然这里选择调用的方法版本是不可能再根据静态类型来决定的,因为静态类型同样都是 Human 的两个变量 man 和 woman 在调用 say 方法时产生了不同的行为。导致这个现象的原因很明显,是因为这两个变量的实际类型不同invokeinterface 和 invokevirtual 指令的运行时解析过程大致分为以下几步

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

使用 javap 命令输出这段代码的字节码,验证这里 say 方法是通过 invokeinterface 指令调用的:

验证invokeinterface 指令
可以看到,在此例中变量 man 通过 invokeinterface 指令调用 say 方法,然后根据上面 invokeinterface 指令的流程,首先查找 man 对象的实际类型,确定为 Man 类型;再在 Man 类中查找 say 方法,发现方法存在,且访问权限校验通过(已确定具体的调用方法),查找结束。

4. 总结

  1. 方法重载:对于参数名和返回值类型相同,但参数类型不同的方法,遵循静态分派的原则,在编译期就可以根据传入变量的静态类型确定具体调用的方法。

    这里的确定是指“某一阶段”中的确定,如果是像静态分派示例中所写的那样sd.say(man),最终调用方法的确定过程应该是这样的:首先动态的确定所调用的方法实际是哪个类中的方法,然后静态的确定是这个类中的哪个方法

  2. 方法重写:对于被 invokeinterface 和 invokevirtual 指令所调用的方法而言(除了被 final 修饰的方法),具体调用哪个方法只有在运行期才可确定,方法查找的方式可以参照前面给出的流程,invokeinterface 和 invokevirtual 指令可以调用哪些方法,具体可参考:从 JVM 的视角看 Java 之方法调用(一):解析.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值