程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> java方法調用之重載、重寫的調用原理(一)

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

編輯:JAVA綜合教程

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


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

結論

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 -verbZ喎?http://www.Bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vc2UgLWMgU3RhdGljRGlzcGF0Y2hlcjwvcD4NCjxwcmUgY2xhc3M9"brush:java;"> 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 "":()V 7: astore_1 8: new #9; //class StaticDispatcher$Man 11: dup 12: invokespecial #10; //Method StaticDispatcher$Man."":()V 15: astore_2 16: new #11; //class StaticDispatcher$Woman 19: dup 20: invokespecial #12; //Method StaticDispatcher$Woman."":()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-long-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."":()V
   7:   astore_1
   8:   new     #4; //class DynamicDispatcher$Woman
   11:  dup
   12:  invokespecial   #5; //Method DynamicDispatcher$Woman."":()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."":()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."":()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."":()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虛擬機》

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved