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重載是靜態多分派的原因是動態分派是單分派,不關心方法參數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章