多型

編譯類型 與 執行類型

編譯類型,英文是Compile-time type。

執行類型,英文是Runtime type。

什麼是編譯類型?什麼是執行類型?

下方的程式碼,等號左邊是編譯類型,等號右邊是執行類型。

1
2
編譯類型     =  執行類型
Parent parent = new Child();

說白話一點就是,雖然類型是父類別,但實際上指向的是子類別物件。

多型就是,物件的「類型是父類別」,但實際上是用「子類別」的「建構子建立」物件。

Memory Layout

Prerequisites:

下面的程式碼有Animal、Dog,Dog繼承Animal。

1
2
3
4
5
6
7
8
9
10
class Animal {
  int i = 10;  // 父類欄位
  void speak() { 
    System.out.println(i); 
  }
}

class Dog extends Animal {
  int i = 5;  // 子類同名欄位
}

Dog Memory Layout如下:

記憶體開始位置/ 佔記憶體大小/ 型別/ 變數/ 存放的值
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0x010033f8 Dog方法記憶體位址
 12   4    int Animal.i                  10
 16   4    int Dog.i                     5
 20   4        (object alignment gap)    
物件大小Instance size: 24 bytes

發現Dog物件中有二個變數,一個變數值為10,來自父類別Animal.i,另一個變數值為5是自己的Dog.i。

記憶體開始位置/ 佔記憶體大小/ 型別/ 變數/ 存放的值
OFF  SZ   TYPE DESCRIPTION               VALUE
 12   4    int Animal.i                  10
 16   4    int Dog.i                     5

反射

使用反射知道子類繼承那些方法。

javap

使用javap知道執行時物件是誰?執行的方法是來自那個物件。

Dog繼承Animal,執行時使用多型,變數類型是父類Animal,指向Dog物件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Animal {
    public void speak() { System.out.println("Animal sound"); }
    public void eat() { System.out.println("Animal eating"); }
}

class Dog extends Animal {
    @Override 
    public void speak() { System.out.println("Bark!"); }
    public void fetch() { System.out.println("Fetching..."); }
}

public class Test1 {
    public static void main(String[] args) throws Exception {
        Animal a = new Dog();  // 多型
        a.speak();             // 輸出: Bark!
        Thread.sleep(5 * 60 * 1000); // 讓程序一直執行
    }
}

編譯以上程式碼

javac Test1.java

執行javap

javap -c -v Test1 

實際呼叫的是Dog()建構子,但使用父類別Animal.speak()。

   #3 = Methodref          #2.#20      //  Dog.<init>:()V
   #4 = Methodref          #22.#23     // Animal.speak:()V

  #20 = NameAndType        #10:#11     //  "<init>":()V
  #21 = Utf8               Dog
  #22 = Class              #29         //  Animal
  #23 = NameAndType        #30:#11     //  speak:()V

  0: new           #2                  // class Dog
  4: invokespecial #3                  // Method Dog."<init>":()V
  9: invokevirtual #4                  // Method Animal.speak:()V

vtable

在Memory Layout中,從8byte開始到11byte結束,佔了4個byte記憶體空間,存放的是vtable的記憶體位址,什麼是vtable?

儲存Dog物件的所有方法。

以下表格第4行為vtable的記憶體位置。

Dog Memory Layout如下:

記憶體開始位置/ 佔記憶體大小/ 型別/ 變數/ 存放的值
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0x010033f8  <- Dog物件的所有方法 記憶體位址
 12   4    int Animal.i                  10
 16   4    int Dog.i                     5
 20   4        (object alignment gap)    
物件大小Instance size: 24 bytes

由於呼叫vtable語法太困難了,以下的vtable是AI模擬出來。

從下面可以發現,Dog有三個方法,分別是equals(), hashCode(), speak()。

vtable for Dog:
[0] Object.equals()@addr1    # 繼承自 Object
[1] Object.hashCode()@addr2  # 繼承自 Object
[2] Animal.speak()@addr3     # 繼承自 Animal

由於Dog沒有覆寫父類別的speak()方法,因為在vtable中,speak()方法是來自父類別Animal。

因此以下程式碼執行時,會去看Dog的vtable中的speak()方法,然後才知道要去呼叫Animal.speak(),因為是呼叫Animal類別,所以使用的是Animal類別中的i屬性,印出的結果是10。

1
2
3
4
5
6
7
8
9
10
class Animal {
  int i = 10;  // 父類欄位
  void speak() { 
    System.out.println(i); 
  }
}

class Dog extends Animal {
  int i = 5;  // 子類同名欄位
}
1
2
3
4
5
6
public class Test2 {
  public static void main(String[] args) {
    Animal myPet = new Dog();  // 多型
    myPet.speak();  // 輸出:10(非 5!)
  }
}

覆寫之後的vtable

若是Dog類別有覆寫speak()方法,vtable裝的是什麼?

1
2
3
4
class Dog extends Animal {
  int i = 5;  // 子類同名欄位
  void speak() { System.out.println(i); }
}
vtable for Dog:
[0] Animal.equals()@123456    # 繼承自 Object
[1] Animal.hashCode()@234567  # 繼承自 Object
[2] Dog.speak()@345678        

由上面可以發現,vtable[2]已經變成Dog.speak()。

執行以下程式碼,結果為5,因為是呼叫Dog類別,所以使用的是Dog類別中的i屬性。

1
2
3
4
5
6
public class Test2 {
  public static void main(String[] args) {
    Animal myPet = new Dog();  // 多型
    myPet.speak();
  }
}

父類別轉型子類別

自動轉型

子類別轉父類別只會有一個父類別,因此使用自動轉型。

1
2
// 以下是自動轉型
Animal myPet = new Dog();

強制轉型

父類別下面會有多個子類別,不確定要轉成那個子類別,因此採用強制轉型,也就是使用括號,括號中是要轉型的(子類別)。

1
2
3
Animal myPet = new Dog();
// 強制轉型成子類別
Dog dog = (Dog) myPet;

強轉回子類別就可以用子類別的屬性與方法。

results matching ""

    No results matching ""