1. 程式人生 > >從字節碼角度分析重載與重寫

從字節碼角度分析重載與重寫

字節 常量池 開始 .text 方法區 stat special 以及 ora

目錄

  • 從字節碼角度分析重載與重寫
    • 代碼分析
    • 字節碼分析
    • 驗證

從字節碼角度分析重載與重寫

代碼分析

/**
    方法的靜態分派
    Grandpa g1 = new Father();
    以上的代碼,g1的靜態類型是Grandpa,而g1得實際類型(真正指向的類型)是Father.
    我們可以得出這樣一個結論:變量的靜態類型是不會發生變化的,而變量的實際類型則是可以發生變化的
    (多態的一種體現),實際類型實在運行期可確定
 */
public class Test5 {
    //方法重載,是一種靜態的行為,這種靜態行為,在編譯期可以完全確定
    //方法重寫,則是一種動態的行為
    public void test(Grandpa grandpa) {
        System.out.println("grandpa");
    }
    public void test(Father father) {
        System.out.println("father");
    }
    public void test(Son son) {
        System.out.println("son");
    }
    public static void main(String[] args) {
        Grandpa g1 = new Father();
        Grandpa g2 = new Son();
        Test5 test5 = new Test5();
        /**
         * 這裏我們是不是一開始以為會輸出father與son?
         * 但是實際輸出的grandpa與grandpa
         * 如何從理論推導?
         */
         //26 invokevirtual #13 <com/chen/jvm/bytecode/Test5.test>
        test5.test(g1);  //grandpa
        test5.test(g2);  //grandpa
    }
}

class Grandpa {
}
class Father extends Grandpa {
}
class Son extends Father {
}
public class Test6 {
    public static void main(String[] args) {
        Fruit apple = new Apple();
        Fruit oranage = new Oranage();

        //17 invokevirtual #6 <com/chen/jvm/bytecode/Fruit.test>
        /**
            從字節碼中我們可以看出,其在編譯器指向的是Fruit.text
            方法的動態分派
            方法的動態分派涉及到一個重要概念:方法接收者
            invokevirtual字節碼指令的多態查找流程
            1.找到操作數棧頂去尋找到棧頂的元素對象所指向的實際類型
            2.如果在實際類型的常量池找到是否有對應的方法以及權限通過,如果符號就返回該對象的引用
            3.如果沒有找到,就沿著這個流程找其父類

            比較方法重載與方法重寫,我們可以得到結論
            方法重載是靜態的,是編譯器行為;方法重寫是動態的,是運行期行為
         */
        apple.test();//Apple
        oranage.test();//Oranage

        apple = new Oranage();
        apple.test();//Oranage
    }
}
class Fruit {
    public void test() {
        System.out.println("Fruit");
    }
}
class Apple extends Fruit {
    //多態,是一種動態的行為
    @Override
    public void test() {
        System.out.println("Apple");
    }
}
class Oranage extends Fruit {
    @Override
    public void test() {
        System.out.println("Oranage");
    }
}

字節碼分析

符號引用,直接引用
有些符號引用是在類加載階段或是第一次使用時就會轉換為直接引用,這種轉換叫做靜態解析;
另外一些符號引用則是在每次運行期直接轉換為直接引用,這種轉換叫做動態鏈接,這體現為Java的多態性
相關字節碼說明

1.invokeinterface:調用接口中的方法,實際上是在運行期決定的,決定到底調用實現該接口的那個對象的特定方法
2.invokestatic:調用靜態方法
3.invokespecial:調用自己的私有方法、構造方法(<init>)以及父類的方法
4.invokevirtual:調用虛方法,運行期動態查找的過程(java多態的基礎)
5.invokedynamic:動態調用方法(jdk1.7引進的)
靜態解析(字節碼還沒有運行的時候,就可以解析,就可以確定調用那個對象的方法)的四種情形:
    1.靜態方法
    2.父類方法
    3.構造方法
    4.私有方法(因為公有方法是有可能被重寫的)
    以上4類方法稱為非虛方法,他們是在類加載階段就可以將符號引用轉換為直接引用的。

Test5.class的Main方法的Code

public static void main(String[] args) {
    Grandpa g1 = new Father();
    Grandpa g2 = new Son();
    Test5 test5 = new Test5();
    test5.test(g1);
    test5.test(g2);
}
 0 new #7 <com/chen/jvm/bytecode/Father>
 3 dup
 //調用自己的私有方法,構造方法
 4 invokespecial #8 <com/chen/jvm/bytecode/Father.<init>>
 7 astore_1
 8 new #9 <com/chen/jvm/bytecode/Son>
11 dup
12 invokespecial #10 <com/chen/jvm/bytecode/Son.<init>>
15 astore_2
16 new #11 <com/chen/jvm/bytecode/Test5>
19 dup
20 invokespecial #12 <com/chen/jvm/bytecode/Test5.<init>>
23 astore_3
24 aload_3
25 aload_1
26 invokevirtual #13 <com/chen/jvm/bytecode/Test5.test>
29 aload_3
30 aload_2
31 invokevirtual #13 <com/chen/jvm/bytecode/Test5.test>
34 return

Test6.class的Main方法的Code

 3 dup
 4 invokespecial #3 <com/chen/jvm/bytecode/Apple.<init>>
 7 astore_1
 8 new #4 <com/chen/jvm/bytecode/Oranage>
11 dup
12 invokespecial #5 <com/chen/jvm/bytecode/Oranage.<init>>
15 astore_2
16 aload_1
//這裏也是同上面一樣的
17 invokevirtual #6 <com/chen/jvm/bytecode/Fruit.test>
20 aload_2
21 invokevirtual #6 <com/chen/jvm/bytecode/Fruit.test>
24 new #4 <com/chen/jvm/bytecode/Oranage>
27 dup
28 invokespecial #5 <com/chen/jvm/bytecode/Oranage.<init>>
31 astore_1
32 aload_1
33 invokevirtual #6 <com/chen/jvm/bytecode/Fruit.test>
36 return

我們從字節碼分析,方法的重載與重寫其底層的字節碼都是invokevirtual:調用虛方法,運行期動態查找的過程。那麽為什麽結果不一樣呢?

/**
  從字節碼中我們可以看出,其在編譯器指向的是Fruit.test
   方法的動態分派
   方法的動態分派涉及到一個重要概念:方法接收者。這一點很重要。
   invokevirtual字節碼指令的多態查找流程
   1.找到操作數棧頂去尋找到棧頂的元素對象所指向的實際類型
   2.如果在實際類型的常量池找到是否有對應的方法以及權限通過,如果符號就返回該對象的引用
   3.如果沒有找到,就沿著這個流程找其父類
   比較方法重載與方法重寫,我們可以得到結論
   方法重載是靜態的,是編譯器行為;方法重寫是動態的,是運行期行為
*/
//方法重載
Test5 test5 = new Test5();
test5.test(g1);  //其方法的接收者是Test5,
test5.test(g2);
//方法重寫中
apple.test();//其方法的接收者是Fruit,會找到操作數棧頂去尋找到棧頂的元素對象所指向的實際類型。所以真正調用Apple中的test方法
oranage.test();

驗證

/**
    針對於方法調用動態分配的過程,虛擬機會在類的方法區建立一個虛方法表的數據結構(virtual method table)
    針對於invokeinterface指令來說,虛擬機會建立一個叫做接口方法表的數據結構
 */
public class Test7 {
    public static void main(String[] args) {
        Animal animal = new Animal();
        Animal dog = new Dog();
        animal.test("hhh");//animal str
        dog.test(new Date());//dog date

        //下面這行代碼在編譯期就不能通過,原因是為什麽呢?
        //因為在字節碼對應的是一個invokevirtual,會編譯成<com/chen/jvm/bytecode.Animal.test2>,
        // 而Animal中並沒有test2這個方法 ,所以編譯也是通不過的。
//        dog.test2();
    }
}
class Animal {
    public void test(String str) {
        System.out.println("animal str");
    }
    public void test(Date date) {
        System.out.println("animal date");
    }
}
class Dog extends Animal {
    @Override
    public void test(String str) {
        System.out.println("dog str");
    }
    @Override
    public void test(Date date) {
        System.out.println("dog date");
    }
    public void test2() {
    }
}

從字節碼角度分析重載與重寫