4段代码了解Java虚拟机虚方法和非虚方法的分派
2017-04-04 19:07
281 查看
先从2段代码聊起,
两段代码唯一一处不同的地方在于代码1的父类Super中的interestingMethod()是private void方法,而代码2中父类Super的interestingMethod()方法为void方法。
那么,这两段代码的输出结果会一样吗?
第一段代码的输出
可以看到,第一段代码调用了父类的interestingMethod方法。
第二段代码的输出:
第二段代码则调用了子类的interestingMethod方法。
为什么会这样呢?这里需要说到Java里哪些是虚方法,哪些是非虚方法?虚方法又如何分派? 除了静态方法之外,声明为final或者private的实例方法是非虚方法。其它(其他非private方法)实例方法都是虚方法。
虚方法和非虚方法的调用又有什么区别呢?在Java 虚拟机里面提供了5条方法调用字节码指令,分别如下:
invokestatic:调用静态方法
invokespecial:调用实例构造器方法,私有方法和父类方法等非虚方法
invokevirtual:调用所有的虚方法
invokeinterface:调用所有的接口方法
invokedynamic:动态运行解析
对非虚方法的调用,程序在编译时,就可以唯一确定一个可调用的版本,且这个方法在运行期不可改变,那么会在类加载的解析阶段,通过前面的指令1,指令2将对这个方法的符号引用转为对应的直接引用,即转为直接引用方法。在Java中,静态方法,final方法和private方法 都是不可在子类中重写的。所以他们都是非虚方法。
代码1中的非虚方法调用的指令(…表示省略了一些上下文)javap -verbose Sub
代码2中的虚方法调用的指令(…表示省略了一些上下文)javap -verbose Sub
invokevirtual的语义是要尝试做虚方法分派,而invokespecial不尝试做虚方法分派。 即invokevirtual调用的方法需要在运行时,根据目标对象的实际类型(代码2中为sub)来动态判断需要执行哪个方法。而invokespecial则只根据常量池中对应序号是哪个方法就执行哪个方法(即看静态类型)。 这里有特殊的一点是,final方法是使用invokevirtual指令来调用的,但是由于它无法被覆盖(不存在其他版本),所以也无须对方法接收者进行多态选择,或者说多态选择的结果是唯一的。在Java语言规范中明确说明了final方法是一种非虚方法
总结起来就是,非虚方法调用只看对象的静态类型。
那虚方法调用呢?结论是invokevirtual调用分2步,第一步在编译期先看方法调用者和参数的静态类型,第二步在运行期再看且只看方法调用者的动态类型。
还有另外一种解释是,所有依赖静态类型来定位方法执行版本的分派动作称为静态分派,静态分派的典型例子是方法重载。
接收者的动态类型来动态分派,即会分派Man/Women的sayHello()方法
1.非虚方法(所有static方法+final/private 方法)通过invokespecial指令调用(final虽然是非虚方法,但是通过invokevirtual调用),不尝试做虚方法分派,对这个非虚方法的符号引用将转为对应的直接引用,即转为直接引用方法,在编译完成时就确定唯一的调用方法。
2.虚方法通过invokevirtual指令调用,且会有分派。具体先根据编译期时方法接收者和方法参数的静态类型来分派,再在运行期根据只根据方法接收者的实际类型来分派,即Java语言是静态多分派,动态单分派类型的语言。需要注意的是,在运行时,虚拟机只关心方法的实际接收者,不关心方法的参数,只根据方法接收者的实际类型来分派。
那么问题来了:
这段代码又会输出什么?
还有一点,为什么Java方法的重载是静态多分派?因为动态单分派时不关心方法的参数,只关心方法的接收者。而方法重载是方法名一样,方法参数不一样,也就导致无法做到动态分派。所以Java重载是静态多分派的原因是动态分派是单分派,不关心方法参数。
代码1:
public class SuperTest { public static void main(String[] args) { new Sub().exampleMethod(); } } class Super { private void interestingMethod() { System.out.println("Super's interestingMethod"); } void exampleMethod() { interestingMethod(); } } class Sub extends Super { void interestingMethod() { System.out.println("Sub's interestingMethod"); } }
代码2:
public class SuperTest { public static void main(String[] args) { new Sub().exampleMethod(); } } class Super { void interestingMethod() { System.out.println("Super's interestingMethod"); } void exampleMethod() { interestingMethod(); } } class Sub extends Super { void interestingMethod() { System.out.println("Sub's interestingMethod"); } }
两段代码唯一一处不同的地方在于代码1的父类Super中的interestingMethod()是private void方法,而代码2中父类Super的interestingMethod()方法为void方法。
那么,这两段代码的输出结果会一样吗?
第一段代码的输出
Super's interestingMethod
可以看到,第一段代码调用了父类的interestingMethod方法。
第二段代码的输出:
Sub's interestingMethod
第二段代码则调用了子类的interestingMethod方法。
为什么会这样呢?这里需要说到Java里哪些是虚方法,哪些是非虚方法?虚方法又如何分派? 除了静态方法之外,声明为final或者private的实例方法是非虚方法。其它(其他非private方法)实例方法都是虚方法。
虚方法和非虚方法的调用又有什么区别呢?在Java 虚拟机里面提供了5条方法调用字节码指令,分别如下:
invokestatic:调用静态方法
invokespecial:调用实例构造器方法,私有方法和父类方法等非虚方法
invokevirtual:调用所有的虚方法
invokeinterface:调用所有的接口方法
invokedynamic:动态运行解析
对非虚方法的调用,程序在编译时,就可以唯一确定一个可调用的版本,且这个方法在运行期不可改变,那么会在类加载的解析阶段,通过前面的指令1,指令2将对这个方法的符号引用转为对应的直接引用,即转为直接引用方法。在Java中,静态方法,final方法和private方法 都是不可在子类中重写的。所以他们都是非虚方法。
代码1中的非虚方法调用的指令(…表示省略了一些上下文)javap -verbose Sub
... Constant pool: ... #30 = Methodref #1.#31 // jvmbook/Super.interestingMethod:()V ... void exampleMethod(); flags: Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #30 // Method interestingMethod:()V 4: return LineNumberTable: line 16: 0 line 17: 4 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Ljvmbook/Super;
代码2中的虚方法调用的指令(…表示省略了一些上下文)javap -verbose Sub
... Constant pool: ... #30 = Methodref #1.#31 // jvmbook/Super.interestingMethod:()V ... void exampleMethod(); ... 1: invokevirtual #30 // Method interestingMethod:()V 4: return ...
Super su =new Sub(); //前面的Super称为su的静态类型,后面的Sub称为su的实际类型
invokevirtual的语义是要尝试做虚方法分派,而invokespecial不尝试做虚方法分派。 即invokevirtual调用的方法需要在运行时,根据目标对象的实际类型(代码2中为sub)来动态判断需要执行哪个方法。而invokespecial则只根据常量池中对应序号是哪个方法就执行哪个方法(即看静态类型)。 这里有特殊的一点是,final方法是使用invokevirtual指令来调用的,但是由于它无法被覆盖(不存在其他版本),所以也无须对方法接收者进行多态选择,或者说多态选择的结果是唯一的。在Java语言规范中明确说明了final方法是一种非虚方法
总结起来就是,非虚方法调用只看对象的静态类型。
那虚方法调用呢?结论是invokevirtual调用分2步,第一步在编译期先看方法调用者和参数的静态类型,第二步在运行期再看且只看方法调用者的动态类型。
代码3:
public class StaticSDispatch { static abstract class Human {} static class Man extends Human {} static class Woman extends Human {} public void sayHello(Human guy) { System.out.println("hello,guy"); } public void sayHello(Man man) { System.out.println("hello,man"); } public void sayHello(Woman woman) { System.out.println("hello,woman"); } public static void main(String[] args) { Human man = new Man(); Human women = new Woman(); StaticSDispatch sd = new StaticSDispatch(); sd.sayHello(man); sd.sayHello(women); } } //输出结果 hello,guy hello,guy
代码3的解释:
首先sayHello()方法是虚方法,通过invokevirtual指令调用。因为在编译期只看方法接收者和参数的静态类型,所以在编译完成后,产生了2条指令,选择了sayHello(Human)作为调用目标,并把这个方法的符号引用写到了main()方法里面的2条invokevirtual指令的参数中。然后在运行期,动态选择sd的实际类型,因为在这sd没有父类,所以不用考虑。还有另外一种解释是,所有依赖静态类型来定位方法执行版本的分派动作称为静态分派,静态分派的典型例子是方法重载。
代码3的字节码:
public static void main(java.lang.String[]); ... 26: invokevirtual #51 // Method sayHello:(Ljvmbook/StaticSDispatch$Human;)V 29: aload_3 30: aload_2 31: invokevirtual #51 // Method sayHello:(Ljvmbook/StaticSDispatch$Human;)V ... }
代码4:
public class DynamicDispatch { static abstract class Human{ protected abstract void sayHello(); } static class Man extends Human{ @Override protected void sayHello() { System.out.println("man say hello"); } } static class Women extends Human { @Override protected void sayHello() { System.out.println("woman say hello"); } } public static void main(String[] args) { DynamicDispatch dy =new DynamicDispatch(); Human man =new Man(); Human women =new Women(); man.sayHello(); women.sayHello(); man =new Women(); man.sayHello(); } } //输出结果 man say hello woman say hello woman say hello
代码4的解释:
首先,sayHello()是虚方法,所以调用指令是invokevirtual.因为该方法没有参数,且方法接收者man/women的实际类型是Human,所以在编译期完成后会产生2条指令:Human.sayHello();然后在动态运行时,只根据方法接收者的动态类型来动态分派,即会分派Man/Women的sayHello()方法
总结:
根据4段代码总结起来就是几句话:1.非虚方法(所有static方法+final/private 方法)通过invokespecial指令调用(final虽然是非虚方法,但是通过invokevirtual调用),不尝试做虚方法分派,对这个非虚方法的符号引用将转为对应的直接引用,即转为直接引用方法,在编译完成时就确定唯一的调用方法。
2.虚方法通过invokevirtual指令调用,且会有分派。具体先根据编译期时方法接收者和方法参数的静态类型来分派,再在运行期根据只根据方法接收者的实际类型来分派,即Java语言是静态多分派,动态单分派类型的语言。需要注意的是,在运行时,虚拟机只关心方法的实际接收者,不关心方法的参数,只根据方法接收者的实际类型来分派。
那么问题来了:
public class Dispatcher { static class QQ { } static class _360 { } public static class Father { public void hardChoice(QQ qq) { System.out.println("father choose qq"); } public void hardChoice(_360 _360) { System.out.println("father choose 360"); } } public static class Son extends Father{ public void hardChoice(QQ qq) { System.out.println("son choose qq"); } public void hardChoice(_360 _360) { System.out.println("son choose 360"); } } public static void main(String[] args) { Father father = new Father(); Father son = new Son(); father.hardChoice(new _360()); son.hardChoice(new QQ()); } }
这段代码又会输出什么?
还有一点,为什么Java方法的重载是静态多分派?因为动态单分派时不关心方法的参数,只关心方法的接收者。而方法重载是方法名一样,方法参数不一样,也就导致无法做到动态分派。所以Java重载是静态多分派的原因是动态分派是单分派,不关心方法参数。
相关文章推荐
- 如何看待Linus “从不认为阅读别人的代码是了解某个想法的一种有用的方法” 言论?
- 新入公司 问问题 ,快速了解代码的方法
- JAVA虚拟机中的方法分派,内联,与解析
- 除了会写代码,这些软件开发需求梳理流程方法你也应该了解!
- 简单完整的代码,通过这个代码你将对RSA加密算法在Java中的实现方法有一个初步的了解,这个类,你可以直接使用,水平高的,就自己修改完善下代码。
- C# 从CIL代码了解委托,匿名方法,Lambda 表达式和闭包本质
- 读coco数据集的代码接口了解segmentation的处理方法
- 【转贴】C++代码优化方法总结
- 四种动态生成Java代码的方法(一)
- Visual InterDev 6.0中调试ASP代码的设置方法
- [导入]对编写安全的代码这个方法有用吗?
- 防止C代码内存泄露的方法
- 完美实现个人建站梦想 全面了解IIS组建方法
- C# 2.0:使用匿名方法、迭代程序和局部类来创建优雅的代码
- 匿名方法代码示例
- 实现方法重载的测试代码
- 各种的jsp数据库连接方法代码!(以前收集的)
- 对于构造方法的简单代码示例
- 我想问一下如何直接连接ACCESS数据库,不用ODBC的方法?能提供代码吗?
- 一段动态获得和执行方法的代码