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

    先从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条方法调用字节码指令,分别如下:

  1. invokestatic:调用静态方法
  2. invokespecial:调用实例构造器方法,私有方法和父类方法等非虚方法
  3. invokevirtual:调用所有的虚方法
  4. invokeinterface:调用所有的接口方法
  5. 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重载是静态多分派的原因是动态分派是单分派,不关心方法参数。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值