計算機程序的思惟邏輯 (17) - 繼承實現的基本原理

本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連接 html

第15節咱們介紹了繼承和多態的基本概念,而上節咱們進一步介紹了繼承的一些細節,本節咱們經過一個例子,來介紹繼承實現的基本原理。須要說明的是,本節主要從概念上來介紹原理,實際實現細節可能與此不一樣。java

例子

這是基類代碼:編程

public class Base {
    public static int s;
    private int a;
    
    static {
        System.out.println("基類靜態代碼塊, s: "+s);
        s = 1;
    }
    
    {
        System.out.println("基類實例代碼塊, a: "+a);
        a = 1;
    }
    
    public Base(){
        System.out.println("基類構造方法, a: "+a);
        a = 2;
    }
    
    protected void step(){
        System.out.println("base s: " + s +", a: "+a);
    }
    
    public void action(){
        System.out.println("start");
        step();
        System.out.println("end");
    }
}
複製代碼

Base包括一個靜態變量s,一個實例變量a,一段靜態初始化代碼塊,一段實例初始化代碼塊,一個構造方法,兩個方法step和action。bash

這是子類代碼:微信

public class Child extends Base {
    public static int s;
    private int a;
    
    static {
        System.out.println("子類靜態代碼塊, s: "+s);
        s = 10;
    }
    
    {
        System.out.println("子類實例代碼塊, a: "+a);
        a = 10;
    }
    
    public Child(){
        System.out.println("子類構造方法, a: "+a);
        a = 20;
    }
    
    protected void step(){
        System.out.println("child s: " + s +", a: "+a);
    }
}
複製代碼

Child繼承了Base,也定義了和基類同名的靜態變量s和實例變量a,靜態初始化代碼塊,實例初始化代碼塊,構造方法,重寫了方法step。函數

這是使用的代碼:佈局

public static void main(String[] args) {
    System.out.println("---- new Child()");
    Child c = new Child();
    
    System.out.println("\n---- c.action()");
    c.action();
    
    Base b = c;
    System.out.println("\n---- b.action()");
    b.action();
    
    
    System.out.println("\n---- b.s: " + b.s); 
    System.out.println("\n---- c.s: " + c.s); 
}
複製代碼

建立了Child類型的對象,賦值給了Child類型的引用變量c,經過c調用action方法,又賦值給了Base類型的引用變量b,經過b也調用了action,最後經過b和c訪問靜態變量s並輸出。這是屏幕的輸出結果:優化

---- new Child()
基類靜態代碼塊, s: 0
子類靜態代碼塊, s: 0
基類實例代碼塊, a: 0
基類構造方法, a: 1
子類實例代碼塊, a: 0
子類構造方法, a: 10

---- c.action()
start
child s: 10, a: 20
end

---- b.action()
start
child s: 10, a: 20
end

---- b.s: 1

---- c.s: 10
複製代碼

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

類的加載

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

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

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

類初始化代碼包括:

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

實例初始化代碼包括:

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

類加載過程包括:

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

須要說明的是,關於類初始化代碼,是先執行父類的,再執行子類的,不過,父類執行時,子類靜態變量的值也是有的,是默認值。對於默認值,咱們以前說過,數字型變量都是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()指向本類中的代碼。

這個表在類加載的時候生成,當經過對象動態綁定方法的時候,只須要查找這個表就能夠了,而不須要挨個查找每一個父類。

接下來,咱們看對變量的訪問。

變量訪問

對變量的訪問是靜態綁定的,不管是類變量仍是實例變量。代碼中演示的是類變量:b.s和c.s,經過對象訪問類變量,系統會轉換爲直接訪問類變量Base.s和Child.s。

例子中的實例變量都是private的,不能直接訪問,若是是public的,則b.a訪問的是對象中Base類定義的實例變量a,而c.a訪問的是對象中Child類定義的實例變量a。

小結

本節,咱們經過一個例子,介紹了類的加載、對象建立、方法調用以及變量訪問的內部過程。如今,咱們應該對繼承的實現有了一個比較清楚的理解。

以前咱們提到過,繼承實際上是把雙刃劍,爲何這麼說呢?讓咱們下節來探討。


未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。用心原創,保留全部版權。

相關文章
相關標籤/搜索