java方法調用之重載、重寫的調用原理

前一段時間看了《深入理解JVM》第三部分虛擬機執行子系統的內容,看到了重載與重寫在JVM層面的調用原理(詳見8.3 方法調用一節),但是沒有寫成博客總結一下,這裏討論討論。在討論過程中,難免會涉及到 字節碼指令 相關的內容,這部分內容請查看博文: 由常量池 運行時常量池 String intern方法想到的(二)之class文件及字節碼指令

結論

1.重載(overload)方法
對重載方法的調用主要看靜態類型,靜態類型是什麼類型,就調用什麼類型的參數方法。
2.重寫(override)方法
對重寫方法的調用主要看實際類型。實際類型如果實現了該方法則直接調用該方法,如果沒有實現,則在繼承關係中從低到高搜索有無實現。
3.
java文件的編譯過程中不存在傳統編譯的連接過程,一切方法調用在class文件中存放的只是符號引用,而不是方法在實際運行時內存佈局中的入口地址。

基本概念

1.靜態類型與實際類型,方法接收者

Human man = new Man();
man.foo();

上面這條語句中,man的靜態類型爲Human,實際類型爲Man。所謂方法接收者,就是指將要執行foo()方法的所有者(在多態中,有可能是父類Human的對象,也可能是子類Man的對象)。
2.字節碼的方法調用指令
(1)invokestatic:調用靜態方法
(2)invokespecial:調用實例構造器方法,私有方法和父類方法。
(3)invokevirtual:調用所有的虛方法。
(4)invokeinterface:調用接口方法,會在運行時再確定一個實現此接口的對象。
(5)invokedynamic:先在運行時動態解析出調用點限定符所引用的方法,然後再執行該方法。
前2條指令(invokestatic, invokespecial),在類加載時就能把符號引用解析爲直接引用,符合這個條件的有靜態方法、實例構造器方法、私有方法、父類方法這4類,這4類方法叫非虛方法。
非虛方法除了上面靜態方法、實例構造器方法、私有方法、父類方法這4種方法之外,還包括final方法。雖然final方法使用invokevirtual指令來調用,但是final方法無法被覆蓋,沒有其他版本,無需對方法接收者進行多態選擇,或者說多態選擇的結果是唯一的。

重載overload

上面說的靜態類型和動態類型都是可以變化的。靜態類型發生變化(強制類型轉換)時,對於編譯器是可知的,即編譯器知道對象的最終靜態類型。而實際類型變化(對象指向了其他對象)時,編譯器是不可知的,只有在運行時纔可知。

//靜態類型變化
sr.sayHello((Man) man);
sr.sayHello((Woman) man);
//實際類型變化
Human man = new Man();
man = new Woman();

    重載只涉及靜態類型的選擇。
    測試代碼如下:

    /**
     * Created by fan on 2016/3/28.
     */
    public class StaticDispatcher {
    
        static class Human {}
        static class Man extends Human {}
        static class Woman extends Human {}
    
        public void sayHello(Human human) {
            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) {
            StaticDispatcher staticDispatcher = new StaticDispatcher();
            Human man = new Man();
            Human woman = new Woman();
            staticDispatcher.sayHello(man);
            staticDispatcher.sayHello(woman);
            staticDispatcher.sayHello((Man)man);
            staticDispatcher.sayHello((Woman)man);
        }
    }

    先看看執行結果:
    這裏寫圖片描述

    由此可見,當靜態類型發生變化時,會調用相應類型的方法。但是,當將Man強制類型轉換成Woman時,沒有編譯錯誤,卻有運行時異常。“classCastException”類映射異常。
    看看字節碼指令:
    javap -verbose -c StaticDispatcher

    public static void main(java.lang.String[]);
      Code:
       Stack=2, Locals=4, Args_size=1
       0:   new     #7; //class StaticDispatcher
       3:   dup
       4:   invokespecial   #8; //Method "<init>":()V
       7:   astore_1
       8:   new     #9; //class StaticDispatcher$Man
       11:  dup
       12:  invokespecial   #10; //Method StaticDispatcher$Man."<init>":()V
       15:  astore_2
       16:  new     #11; //class StaticDispatcher$Woman
       19:  dup
       20:  invokespecial   #12; //Method StaticDispatcher$Woman."<init>":()V
       23:  astore_3
       24:  aload_1
       25:  aload_2
       26:  invokevirtual   #13; //Method sayHello:(LStaticDispatcher$Human;)V
       29:  aload_1
       30:  aload_3
       31:  invokevirtual   #13; //Method sayHello:(LStaticDispatcher$Human;)V
       34:  aload_1
       35:  aload_2
       36:  checkcast       #9; //class StaticDispatcher$Man
       39:  invokevirtual   #14; //Method sayHello:(LStaticDispatcher$Man;)V
       42:  aload_1
       43:  aload_2
       44:  checkcast       #11; //class StaticDispatcher$Woman
       47:  invokevirtual   #15; //Method sayHello:(LStaticDispatcher$Woman;)V
       50:  return

      看到,在強制類型轉換時,會有指令checkCast的調用,而且invokevirtual指令的調用方法也發生了變化39: invokevirtual #14; //Method sayHello:(LStaticDispatcher$Man;)V
      虛擬機(準確說是編譯器)在重載時是通過參數的靜態類型而不是實際類型作爲判定依據的。
      對於字面量類型,編譯器會自動進行類型轉換。轉換的順序爲:
      char-int-long-float-double-Character-Serializable-Object
      轉換成Character是因爲發生了自動裝箱,轉換成Serializable是因爲Character實現了Serializable接口。

      重寫override

      測試代碼如下:

      /**
       * Created by fan on 2016/3/29.
       */
      public class DynamicDispatcher {
      
          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 Woman extends Human {
      
              @Override
              protected void sayHello() {
                  System.out.println("Woman say hello");
              }
          }
      
          public static void main(String[] args) {
              Human man = new Man();
              Human woman = new Woman();
              man.sayHello();
              woman.sayHello();
              man = new Woman();
              man.sayHello();
          }
      
      }

      執行結果:
      這裏寫圖片描述

      看下字節碼指令:

      public static void main(java.lang.String[]);
        Code:
         Stack=2, Locals=3, Args_size=1
         0:   new     #2; //class DynamicDispatcher$Man
         3:   dup
         4:   invokespecial   #3; //Method DynamicDispatcher$Man."<init>":()V
         7:   astore_1
         8:   new     #4; //class DynamicDispatcher$Woman
         11:  dup
         12:  invokespecial   #5; //Method DynamicDispatcher$Woman."<init>":()V
         15:  astore_2
         16:  aload_1
         17:  invokevirtual   #6; //Method DynamicDispatcher$Human.sayHello:()V
         20:  aload_2
         21:  invokevirtual   #6; //Method DynamicDispatcher$Human.sayHello:()V
         24:  new     #4; //class DynamicDispatcher$Woman
         27:  dup
         28:  invokespecial   #5; //Method DynamicDispatcher$Woman."<init>":()V
         31:  astore_1
         32:  aload_1
         33:  invokevirtual   #6; //Method DynamicDispatcher$Human.sayHello:()V
         36:  return

      從字節碼中可以看到,他們調用的都是相同的方法invokevirtual #6; //Method DynamicDispatcher$Human.sayHello:()V ,但是執行的結果卻顯示調用了不同的方法。因爲,在編譯階段,編譯器只知道對象的靜態類型,而不知道實際類型,所以在class文件中只能確定要調用父類的方法。但是在執行時卻會判斷對象的實際類型。如果實際類型實現這個方法,則直接調用,如果沒有實現,則按照繼承關係從下往上一次檢索,只要檢索到就調用,如果始終沒有檢索到,則拋異常(難道能編譯通過)。

      (1)測試代碼如下:

      /**
       * Created by fan on 2016/3/29.
       */
      public class Test {
      
          static class Human {
              protected void sayHello() {
                  System.out.println("Human say hello");
              }
              protected void sayHehe() {
                  System.out.println("Human say hehe");
              }
          }
      
          static class Man extends Human {
      
              @Override
              protected void sayHello() {
                  System.out.println("Man say hello");
              }
      
      //        protected void sayHehe() {
      //            System.out.println("Man say hehe");
      //        }
          }
      
          static class Woman extends Human {
      
              @Override
              protected void sayHello() {
                  System.out.println("Woman say hello");
              }
      
      //        protected void sayHehe() {
      //            System.out.println("Woman say hehe");
      //        }
          }
      
          public static void main(String[] args) {
              Human man = new Man();
              man.sayHehe();
          }
      
      }

      測試結果如下:
      這裏寫圖片描述
      字節碼指令:

      public static void main(java.lang.String[]);
        Code:
         Stack=2, Locals=2, Args_size=1
         0:   new     #2; //class Test$Man
         3:   dup
         4:   invokespecial   #3; //Method Test$Man."<init>":()V
         7:   astore_1
         8:   aload_1
         9:   invokevirtual   #4; //Method Test$Human.sayHehe:()V
         12:  return

      字節碼指令與上面代碼的字節碼指令沒有本質區別。

      (2)測試代碼如下:

      /**
       * Created by fan on 2016/3/29.
       */
      public class Test {
      
          static class Human {
              protected void sayHello() {
              }
          }
      
          static class Man extends Human {
      
              @Override
              protected void sayHello() {
                  System.out.println("Man say hello");
              }
      
              protected void sayHehe() {
                  System.out.println("Man say hehe");
              }
          }
      
          static class Woman extends Human {
      
              @Override
              protected void sayHello() {
                  System.out.println("Woman say hello");
              }
      
              protected void sayHehe() {
                  System.out.println("Woman say hehe");
              }
          }
      
          public static void main(String[] args) {
              Human man = new Man();
              man.sayHehe();
          }
      
      }

      編譯時報錯:
      這裏寫圖片描述

      這個例子說明了:Java編譯器是基於靜態類型進行檢查的。

      修改上面錯誤代碼,如下所示:

      /**
       * Created by fan on 2016/3/29.
       */
      public class Test {
      
          static class Human {
              protected void sayHello() {
                  System.out.println("Human say hello");
              }
      //        protected void sayHehe() {
      //            System.out.println("Human say hehe");
      //        }
          }
      
          static class Man extends Human {
      
              @Override
              protected void sayHello() {
                  System.out.println("Man say hello");
              }
      
              protected void sayHehe() {
                  System.out.println("Man say hehe");
              }
          }
      
          static class Woman extends Human {
      
              @Override
              protected void sayHello() {
                  System.out.println("Woman say hello");
              }
      
              protected void sayHehe() {
                  System.out.println("Woman say hehe");
              }
          }
      
          public static void main(String[] args) {
              Man man = new Man();
              man.sayHehe();
          }
      
      }

      注意在Main方法中,改成了Man man = new Man();
      執行結果如下所示:
      這裏寫圖片描述
      字節碼指令如下所示:

      public static void main(java.lang.String[]);
        Code:
         Stack=2, Locals=2, Args_size=1
         0:   new     #2; //class Test$Man
         3:   dup
         4:   invokespecial   #3; //Method Test$Man."<init>":()V
         7:   astore_1
         8:   aload_1
         9:   invokevirtual   #4; //Method Test$Man.sayHehe:()V
         12:  return

      注意上面的字節碼指令invokevirtual #4; //Method Test$Man.sayHehe:()V

      結束語

      本文討論了一下重載與重寫的基本原理,查看了相關的字節碼指令,下篇博文 java方法調用之單分派與多分派(二)討論下單分派與多分派。
      可以再看看這篇博文 java方法調用之動態調用多態(重寫override)的實現原理——方法表(三)

      參考資料

      • 周志明 《深入理解JAVA虛擬機》

      轉載:https://blog.csdn.net/fan2012huan/article/details/50999777

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