Java——繼承機制詳談

先給出結論:

子類只能繼承父類的非靜態方法,並可以對之進行覆蓋

對於父類的成員變量和靜態方法,子類不能夠繼承,但是子類可以訪問到父類的成員變量和靜態方法。如果此時子類中有與父類相同的成員變量或靜態方法,也只是把父類的靜態方法隱藏

當通過該變量訪問它所引用的對象的成員變量和靜態方法時,該實例變量的值取決於該變量的聲明類型;

當通過該變量來調用它所引用的對象的非靜態方法時,該方法取決於它實際引用的對象的類型。

 

下面我們來解釋一下背後都發生了一些什麼事情,從類的加載開始。

類的加載

在Java中,所謂類的加載是指將類的相關信息加載到內存。在Java中,類是動態加載的,當第一次使用這個類的時候纔會加載,加載一個類時,會查看其父類是否已加載,如果沒有,則會加載其父類。

一個類的信息主要包括以下部分:

  • 類變量(靜態變量)
  • 類初始化代碼
  • 類方法(靜態方法)
  • 實例變量
  • 實例初始化代碼
  • 實例方法
  • 父類信息引用 

類加載過程包括:

  1. 分配內存保存類的信息
  2. 給類變量賦默認值
  3. 加載父類
  4. 設置父子關係
  5. 執行類初始化代碼 

類初始化代碼包括:

  1. 定義靜態變量時的賦值語句
  2. 靜態初始化代碼塊

實例初始化代碼包括:

  1. 定義實例變量時的賦值語句
  2. 實例初始化代碼塊
  3. 構造方法 

 

需要說明的是,關於類初始化代碼,是先執行父類的,再執行子類的,不過,父類執行時,子類靜態變量的值也是有的,是默認值。對於默認值,我們之前說過,數字型變量都是0,boolean是false,char是'\u0000',引用型變量是null。

之前我們說過,內存分爲棧和堆,棧存放函數的局部變量,而堆存放動態分配的對象,還有一個內存區,存放類的信息,這個區在Java中稱之爲方法區。

加載後,對於每一個類,在Java方法區就有了一份這個類的信息,以我們的例子來說,有三份類信息,分別是Child,Base,Object,內存示意圖如下:

我們用class_init()來表示類初始化代碼,用instance_init()表示實例初始化代碼,實例初始化代碼包括了實例初始化代碼塊和構造方法。例子中只有一個構造方法,實際中可能有多個實例初始化方法。

本例中,類的加載大概就是在內存中形成了類似上面的佈局,然後分別執行了Base和Child的類初始化代碼。接下來,我們看對象創建的過程。

創建對象

在類加載之後,new Child()就是創建Child對象,創建對象過程包括:

  1. 分配內存
  2. 對所有實例變量賦默認值
  3. 執行實例初始化代碼 

分配的內存包括本類和所有父類的實例變量,但不包括任何靜態變量。實例初始化代碼的執行從父類開始,先執行父類的,再執行子類的。但在任何類執行初始化代碼之前,所有實例變量都已設置完默認值。

每個對象除了保存類的實例變量之外,還保存着實際類信息的引用。

Child c = new Child();會將新創建的Child對象引用賦給變量c,而Base b = c;會讓b也引用這個Child對象。創建和賦值後,內存佈局大概如下圖所示:

 

引用型變量c和b分配在棧中,它們指向相同的堆中的Child對象,Child對象存儲着方法區中Child類型的地址,還有Base中的實例變量a和Child中的實例變量a。創建了對象,接下來,來看方法調用的過程。

方法調用

我們先來看c.action();這句代碼的執行過程是:

  1. 查看c的對象類型,找到Child類型,在Child類型中找action方法,發現沒有,到父類中尋找
  2. 在父類Base中找到了方法action,開始執行action方法
  3. action先輸出了start,然後發現需要調用step()方法,就從Child類型開始尋找step方法
  4. 在Child類型中找到了step()方法,執行Child中的step()方法,執行完後返回action方法
  5. 繼續執行action方法,輸出end

尋找要執行的實例方法的時候,是從對象的實際類型信息開始查找的,找不到的時候,再查找父類類型信息。

我們來看b.action();,這句代碼的輸出和c.action是一樣的,這稱之爲動態綁定,而動態綁定實現的機制,就是根據對象的實際類型查找要執行的方法,子類型中找不到的時候再查找父類。這裏,因爲b和c指向相同的對象,所以執行結果是一樣的。

如果繼承的層次比較深,要調用的方法位於比較上層的父類,則調用的效率是比較低的,因爲每次調用都要進行很多次查找。大多數系統使用一種稱爲虛方法表的方法來優化調用的效率。

虛方法表

所謂虛方法表,就是在類加載的時候,爲每個類創建一個表,這個表包括該類的對象所有動態綁定的方法及其地址,包括父類的方法,但一個方法只有一條記錄,子類重寫了父類方法後只會保留子類的。

對於本例來說,Child和Base的虛方法表如下所示:

對Child類型來說,action方法指向Base中的代碼,toString方法指向Object中的代碼,而step()指向本類中的代碼。

這個表在類加載的時候生成,當通過對象動態綁定方法的時候,只需要查找這個表就可以了,而不需要挨個查找每個父類。

 

參考:

《Java編程的邏輯》

 

 

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