多態是面向對象的最主要的特性之一,是一種方法的動態綁定,實現運行時的類型決定對象的行為。多態的表現形式是父類指針或引用指向子類對象,在這個指針上調用的方法使用子類的實現版本。多態是IOC、模板模式實現的關鍵。
在C++中通過虛函數表的方式實現多態,每個包含虛函數的類都具有一個虛函數表(virtual table),在這個類對象的地址空間的最靠前的位置存有指向虛函數表的指針。在虛函數表中,按照聲明順序依次排列所有的虛函數。比如:
class Base {
public:
virtual void f() {
printf("Base::f()");
}
virtual void g() {
printf("Base::g()");
}
};
class Derived: public Base {
public:
virtual void f() {
printf("Derived::f()");
}
};
上面代碼對應的類布局:
由于C++在運行時并不維護類型信息,所以在編譯時直接在子類的虛函數表中將被子類重寫的方法替換掉,如上圖的Derived::f(),這個方法會被放到虛函數表中原來父函數在的位置。由于在編譯時就確定了虛函數在虛表中的下標,所以在進行虛函數調用時,直接根據下標進行訪問。比如,調用Derived對象上的f():
Base *b = new Derived;
b->f();
在調用b->f()時,內部會轉化成(*b->vptr[1])(),由于虛函數表需要完成RTII,所以虛函數表的第一個slot存放的是type info,虛函數下標從1開始。實際上,虛函數表記錄了這個類的所有虛函數的具體實現(就是在運行時確切要調用的),編譯時就可以確定,不需要動態查找,效率較高。
而Java中,在運行時會維持類型信息以及類的繼承體系。每一個類會在方法區中對應一個數據結構用于存放類的信息,可以通過Class對象訪問這個數據結構。其中,類型信息具有superclass屬性指示了其超類,以及這個類對應的方法表(其中只包含這個類定義的方法,不包括從超類繼承來的)。而每一個在堆上創建的對象,都具有一個指向方法區類型信息數據結構的指針,通過這個指針可以確定對象的類型。
JVM中用于方法調用的指令包括:
invokevirtual:用于調用實例方法,會根據對象的實際類型進行調用。
invokespecial:需要特殊處理的實例方法,比如:public final方法、私有方法和父類方法等。調用的方法取決于引用的類型。
invokeinterface:調用接口的方法。
invokestatic:調用類方法。
按照上面描述,對于子類覆蓋父類的方法,編譯后,調用指令應該是invokevirtual,調用的方法取決于對象的類型。invokevirtual方法查找的實現方式是:
1.?通過對象中類指針找到其類信息,然后在方法表中根據方法簽名找到該方法。
2. 如果不在當前類,則遞歸查找其父類的方法表直到Object類。
3. 如果找到Object類,也沒有該方法,會拋出NoSuchMethodException異常。
與js、lua等動態語言類似,Java的實現方式依賴于內存中的類型體系信息,存在一個“原型鏈”,是一個完全動態的查找過程,相對于C++而言,效率會低一些,因為存在一個鏈表遍歷查找的過程。之所以,Java中可以這樣實現,本質上是因為它是一門虛擬機語言,虛擬機會維持所有的這些類型信息。