JAVA多態的動態綁定機制

對於多態,大家基本上都很熟悉,日常開發用的也挺多,一句話概括:父類引用指向子類對象

  • 在集合的使用上,List mList = new ArrayList<>();
  • 在類的繼承時,Anim anim = new Cat();
    爲了弄清楚多態,我們需要引入jvm方法調用的靜態和動態綁定機制的概念,
    jvm靜態綁定機制
Public class Utils{
	private static Utils utils;
	private Utils(){}
	Public static Utils getInstace(){
		utils = new utils();
		return utils ;
	}
}
class testActivity extends Activity{
	@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(setView());
        Utils.getInstace();//靜態綁定就在這裏
    }
}

靜態綁定的這句代碼Utils.getInstance(),會被編譯器編譯成一條指令:invokstatic #13,jvm是如何處理這條指令的?
(1) 指令 #13的含義:testActivity 類常量池中第13個常量表的索引項,關於常量池詳見《Class文件內容及常量池 》,這個常量表記錄的是方法getInstance()信息的符號引用,包括getInstance所在的類名、方法名和返回類型,jvm首先會根據這個符號引用去找到getInstance方法所在類的全包名
(2)緊接着jvm會加載、鏈接、初始化Utils類
(3)然後再testActivity 類所在的方法區找到getInstance()方法的直接地址,且將這個直接地址記錄到testActivity 類常量池中索引爲13的常量表,這個過程稱爲常量池解析,以後再次調用Utils.genInstance()時,直接找到方法的字節碼
(4)完成了類的常量池索引項中常量表解析後,jvm就能調用這個靜態方法,去執行方法邏輯
通過上述幾個步驟後,我們發現當完成常量池解析後,jvm就能確定getInstance方法具體在內存的什麼位置上,事實上這個信息在編譯階段就已經在類的常量池中記錄了下來,在編譯階段能夠確定方法在內存什麼位置的機制就叫靜態綁定機制
*所有私有方法、靜態方法、構造器及final修飾方法都是採用靜態綁定機制。在編譯器階段就已經指明瞭調用方法在常量池中的符號引用,JVM運行的時候只需要進行一次常量池解析即可 *。

jvm動態綁定機制

public class AnimObj {
    private  String name = "anim";
    public  String f1() {
        Log.e("11111111_Anim_getName","Anim_getName");
        return name;
    }
}
//子類
public class DogObj extends AnimObj {
      String name = "dog";
    public  String f1() {
        Log.e("11111111_dog_getName","Dog_getName");
        return name;
    }
}
//多態的調用,
AnimObj anim = new DogObj();
        anim.f1();

首先我們知道,要有多態,它的前提就是繼承、重寫,那麼jvm是如何確保找到的方法是cat 類中而不是Anim類?有點繞口,就是說代碼中類的引用是Anim,正常情況下調用的方法也是Anim類中的方法,多態是怎麼做到把這個方法變成調用Cat類中的方法?在這裏我們先要講一下jvm管理的一個非常重要的數據結構-----方法表
在jvm加載的同時,會在方法區中爲這個類存放很多信息(詳見《Java 虛擬機體系結構 》),其中有一個數據結構叫方法表,它以數組的形式記錄了當前類和所有父類的可見方法字節碼在內存中的直接地址,下圖是對比的兩個方法表
在這裏插入圖片描述
上圖中的方法表有兩個特點:(1) 子類方法表中繼承了父類的方法,比如Anim extends Object。 (2) 相同的方法(相同的方法簽名:方法名和參數列表)在所有類的方法表中的索引相同。比如Father方法表中的f1()和Sun方法表中的f1()都位於各自方法表的第11項中。
對於上面的源代碼,編譯器會將多態調用的方法翻譯成字節碼指令

//(1) Anim a =  new Cat();  //在堆內存中開闢一個Cat對象的內存空間,並將對象引用壓入操作棧
//invokespecial #7 [15]  //調用初始化方法初始化堆中的Cat對象
//astore_1 //彈出操作樹棧的Cat對象引用壓入局部變量表1中
// aload_1 //取出局部變量表1的對象引用壓入操作數棧
// invokevirtual #15   調用getName方法

其中invokevirtual 的詳細指令是這樣的:

  1. invokevirtual #15中的 #15指TestActivity類中常量池第15位置的常量表索引項,這個常量表記錄的是方法f1信息的符號引用(包括 類名及其所有父類、方法名和返回類型),JVM首先會根據方法的符號引用找到調用f1方法的類的全包名(全限定名)com.my_project.internal_obj.obj.AnimObj,因爲這是調用f1方法的類對象anim聲明爲Anim類型。
  2. 在Anim類型的方法表中查找f1方法,如果找到,將f1在方法表中的索引項11記錄到TestActivity類常量池第15位置的常量表中(也就是常量池解析),需要引起注意的是,如果f1這個方法在Anim類中沒有,即使Cat方法中存在,也無法通過編譯,因爲當前類的類型是Anim
  3. 在調用invokevirtual指令前有一個aload_1指令,它會將創建在堆內存中的Cat對象引用壓入操作數棧,然後invokevirtual指令會根據Cat的對象引用首先找到堆內存中的Son對象,進一步找到Cat對象所屬類型的方法表,下圖所示:
    在這裏插入圖片描述
    方法數據存放在類的方法區中,包含一個方法的具體實現的字節碼二進制。方法指針直接指向這個方法在內存中的起始位置,通過方法指針就可以找到這個方法。
  4. 這是通過第二步解析完成的#15常量表中方法表的索引項11,可以定位到Son類型方法表中的f1方法,然後通過直接地址找到該方法字節碼所在的內存空間

類對象方法的調用必須在運行過程中採用動態綁定機制。
首先,根據對象的聲明類型(對象引用的類型)找到“合適”的方法。具體步驟如下:
① 如果能在聲明類型中匹配到方法簽名完全一樣(參數類型一致)的方法,那麼這個方法是最合適的。
② 在第①條不能滿足的情況下,尋找可以“湊合”的方法。標準就是通過將參數類型進行自動轉型之後再進行匹配。如果匹配到多個自動轉型後的方法簽名f(A)和f(B),則用下面的標準來確定合適的方法:傳遞給f(A)方法的參數都可以傳遞給f(B),則f(A)最合適。反之f(B)最合適 。
③ 如果仍然在聲明類型中找不到“合適”的方法,則編譯階段就無法通過。
然後,根據在堆中創建對象的實際類型找到對應的方法表,從中確定具體的方法在內存中的位置。

這是我自己理解的,不知道對不對!!
在編譯期間,Anim對象類型調用的f1方法,是將方法表中的索引項11是指向Anim這個對象類型的,而在運行期間,Anim對象類型在堆內存中創建的實際對象是Cat,所以jvm會根據內存中真實的對象引用重新去給f1的方法表索引項11賦值,這種通過程序運行過程動態創建對象方法表的定位方法的方式,一般稱之爲動態綁定機制。

最後說明,域和靜態方法都是不具有多態性的,任何的域訪問操作都將由編譯器解析,因此不是多態的。靜態方法是跟類,而並非單個對象相關聯的。
參考文章:http://hxraid.iteye.com/blog/428891

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章