4段代码了解Java虚拟机虚方法和非虚方法的分派

    xiaoxiao2021-03-25  74

        先从2段代码聊起,

       代码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重载是静态多分派的原因是动态分派是单分派,不关心方法参数。

    转载请注明原文地址: https://ju.6miu.com/read-23123.html

    最新回复(0)