從字節碼分析java的方法重寫和重載

不知道有沒有小伙伴在面試時被問到過方法重寫(Override)和重載(Overload)的區別?反正我是被問起過數次,大概情況是這樣的:

面試官:說下Override和Overload的區別?
:(內心:額,送人頭的,然后就)方法重寫發生在子、父繼承的關系下,子類可以修改父類的方法,以達到增強、擴展等~~#¥%@ bala bala
面試官:嗯,還有嗎?
:(xx 一緊,還有啥?努力回想是不是忘說了啥)額,就這些吧
面試官:恩,今天面試就到這吧,你回去等消息吧
:#¥%
@~

方法重寫和重載,相信只要是剛接觸過java語言,對這兩個概念就不會陌生,遇到相關的面試題估計也不少,今天我們就從面試題下手,然后再分析到其字節碼層面,對這兩個概念做一個介紹。
首先貼上面試題

// 父類
public class Parent {
    int age = 40;
    public void walk() {
        System.out.println("parent walk");
    }
}

// 子類
public class Child extends Parent {
    int age = 15;
    public void walk() {
        System.out.println("child walk");
    }
}

// 測試重載
public class TestOverload {

    public void method(Parent parent) {
        System.out.println("parent");
    }

    public void method(Child child) {
        System.out.println("child");
    }

    public static void main(String[] args) {
        TestOverload testOverload = new TestOverload();

        Parent parent = new Child();
        testOverload.method(parent);
        Child child = new Child();
        testOverload.method(child);
    }
}

相信機智如你,早就知道了答案,結果:

parent
child

Process finished with exit code 0

不多解釋概念,先javap看下字節碼(為節省篇幅,只貼出部分常量池和main字節碼)

Constant pool:
   #1 = Methodref          #12.#34        // java/lang/Object."<init>":()V
   #2 = Fieldref           #35.#36        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #22            // parent
   #4 = Methodref          #37.#38        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = String             #25            // child
   #6 = Class              #39            // com/jvm/learnjvm/test/TestOverload
   #7 = Methodref          #6.#34         // com/jvm/learnjvm/test/TestOverload."<init>":()V
   #8 = Class              #40            // com/jvm/learnjvm/test/Child
   #9 = Methodref          #8.#34         // com/jvm/learnjvm/test/Child."<init>":()V
  #10 = Methodref          #6.#41         // com/jvm/learnjvm/test/TestOverload.method:(Lcom/jvm/learnjvm/test/Parent;)V
  #11 = Methodref          #6.#42         // com/jvm/learnjvm/test/TestOverload.method:(Lcom/jvm/learnjvm/test/Child;)V
  #12 = Class              #43            // java/lang/Object
  #13 = Utf8               <init>


 public static void main(java.lang.String[]);  // main 方法
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: new           #6                  // class com/jvm/learnjvm/test/TestOverload
         3: dup
         4: invokespecial #7                  // Method "<init>":()V
         7: astore_1
         8: new           #8                  // class com/jvm/learnjvm/test/Child
        11: dup
        12: invokespecial #9                  // Method com/jvm/learnjvm/test/Child."<init>":()V
        15: astore_2
        16: aload_1
        17: aload_2
        18: invokevirtual #10                 // Method method:(Lcom/jvm/learnjvm/test/Parent;)V
        21: new           #8                  // class com/jvm/learnjvm/test/Child
        24: dup
        25: invokespecial #9                  // Method com/jvm/learnjvm/test/Child."<init>":()V
        28: astore_3
        29: aload_1
        30: aload_3
        31: invokevirtual #11                 // Method method:(Lcom/jvm/learnjvm/test/Child;)V


我們重點看一下main方法的 18: invokevirtual #1031: invokevirtual #11,執行的方法,對照常量池的#10 = Methodref #6.#41 // com/jvm/learnjvm/test/TestOverload.method:(Lcom/jvm/learnjvm/test/Parent;)V,
#11 = Methodref #6.#42 // com/jvm/learnjvm/test/TestOverload.method:(Lcom/jvm/learnjvm/test/Child;)V,通過方法入參,可以看到很清楚的看到調用的方法情況,因為此時并沒有運行,不知道將來傳入的參數真實實例是什么,編譯器只是根據聲明的參數的類型和數量等匹配到合適的重載方法,這種方式,被稱為“靜態分派”。
同樣在編譯期確定的還有調用成員變量的經典面試題,有興趣的可以自己看下字節碼分析下

public class TestOverload {

    public void method(Parent parent) {
        System.out.println("parent");
    }

    public void method(Child child) {
        System.out.println("child");
    }

    public static void main(String[] args) {
        TestOverload testOverload = new TestOverload();

        Parent parent = new Child();
        System.out.println(parent.age);
        
        Child child = new Child();
        System.out.println(child.age);
    }
}

結果

40
15

Process finished with exit code 0

分析完方法重載,現在分析下方法重寫,先來一段大家都熟到不能再熟的代碼(Parent和Child類依舊使用之前的)

public class TestOverride {

    public static void main(String[] args) {
        Parent parent = new Child();
        parent.walk();
    }
}

大家用腳指頭想都知道的結果

child walk

Process finished with exit code 0

面對感覺理所應當的結果,我們還是先看看字節碼吧

Constant pool:
   #1 = Methodref          #6.#22         // java/lang/Object."<init>":()V
   #2 = Class              #23            // com/jvm/learnjvm/test/Child
   #3 = Methodref          #2.#22         // com/jvm/learnjvm/test/Child."<init>":()V
   #4 = Methodref          #24.#25        // com/jvm/learnjvm/test/Parent.walk:()V
   #5 = Class              #26            // com/jvm/learnjvm/test/TestOverride
   #6 = Class              #27            // java/lang/Object

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class com/jvm/learnjvm/test/Child
         3: dup
         4: invokespecial #3                  // Method com/jvm/learnjvm/test/Child."<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method com/jvm/learnjvm/test/Parent.walk:()V
        12: return

執行方法調用的是9: invokevirtual #4,指向的是常量池#4,然后我們絲毫不慌的去常量看看,
#4 = Methodref #24.#25 // com/jvm/learnjvm/test/Parent.walk:()V
what???調用的是Parent的walk() ?氣氛突然有些尷尬......
其實這個時候,就要說下面向對象的三大特性:封裝、繼承、多態中的多態的實現原理了。多態是什么不用我說大家也都知道,就不解釋概念了,從字節碼角度說一下,還是回到main方法的字節碼再看一下,前面主要是創建對象,執行構造方法,并把當前創建的對象實例壓到操作數棧,重點看下9,執行invokevirtual指令,(jdk提供了5條方法調用的指令,在最下面有列出),invokevirtual指令是找到當前操作數棧棧頂元素指向的對象的實際類型,也就是new出來的Child,然后執行該指令對應的常量池中的方法#4,而這時候是運行期,jvm會根據方法的名稱和描述來定位方法,調用的是Child實例的walk,這種動態調用方法的方式,也被稱為動態分派。

方法調用指令
invokestatic ?? ??????調用靜態方法
invokevirtual ???????調用實例方法
invokespecial????????調用私有方法、實例構造方法、super()
invokeinterface ????調用引用類型為interface的實例方法
invokedynamic ???????JDK 7引入的,主要是為了支持動態語言的方法調用,如Lambda

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。