Java編程的邏輯 (16) - 繼承的細節

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


上節咱們介紹了繼承和多態的基本概念,基本概念是比較簡單的,子類繼承父類,自動擁有父類的屬性和行爲,並可擴展屬性和行爲,同時,可重寫父類的方法以修改行爲。編程

但繼承和多態概念還有一些相關的細節,本節就來探討這些細節,具體包括:swift

  • 構造方法
  • 重名與靜態綁定
  • 重載和重寫
  • 父子類型轉換
  • 繼承訪問權限 (protected)
  • 可見性重寫
  • 防止繼承 (final)

下面咱們逐個來解釋。設計模式

構造方法安全

super
微信

上節咱們說過,子類能夠經過super(...)調用父類的構造方法,若是子類沒有經過super(...)調用,則會自動調動父類的默認構造方法,那若是父類沒有默認構造方法呢?以下例所示:框架

public class Base {
    private String member;
    public Base(String member){
        this.member = member;
    }
}

這個類只有一個帶參數的構造方法,沒有默認構造方法。這個時候,它的任何子類都必須在構造方法中經過super(...)調用Base的帶參數構造方法,以下所示,不然,Java會提示編譯錯誤。函數

public class Child extends Base {
    public Child(String member) {
        super(member);
    }
}

構造方法調用重寫方法post

若是在父類構造方法中調用了可被重寫的方法,則可能會出現意想不到的結果,咱們來看個例子:
this

這是基類代碼:

public class Base {
    public Base(){
        test();
    }
    
    public void test(){
    }
}

構造方法調用了test()。這是子類代碼:

public class Child extends Base {
    private int a = 123;
    
    public Child(){
    }
    
    public void test(){
        System.out.println(a);
    }
}

子類有一個實例變量a,初始賦值爲123,重寫了test方法,輸出a的值。看下使用的代碼:

public static void main(String[] args){
    Child c = new Child();
    c.test();
}

輸出結果是:

0
123

第一次輸出爲0,第二次爲123。第一行爲何是0呢?第一次輸出是在new過程當中輸出的,在new過程當中,首先是初始化父類,父類構造方法調用 test(),test被子類重寫了,就會調用子類的test()方法,子類方法訪問子類實例變量a,而這個時候子類的實例變量的賦值語句和構造方法還沒 有執行,因此輸出的是其默認值0。

像這樣,在父類構造方法中調用可被子類重寫的方法,是一種很差的實踐,容易引發混淆,應該只調用private的方法。

重名與靜態綁定

上節咱們說到,子類能夠重寫父類非private的方法,當調用的時候,會動態綁定,執行子類的方法。那實例變量、靜態方法、和靜態變量呢?它們能夠重名嗎?若是重名,訪問的是哪個呢?

重名是能夠的,重名後實際上有兩個變量或方法。對於private變量和方法,它們只能在類內被訪問,訪問的也永遠是當前類的,即在子類中,訪問的是子類的,在父類中,訪問的父類的,它們只是碰巧名字同樣而已,沒有任何關係。

但對於public變量和方法,則要看如何訪問它,在類內訪問的是當前類的,但子類能夠經過super.明確指定訪問父類的。在類外,則要看訪問變量的靜態類型,靜態類型是父類,則訪問父類的變量和方法,靜態類型是子類,則訪問的是子類的變量和方法。咱們來看個例子:

這是基類代碼:

public class Base {
    public static String s = "static_base";
    public String m = "base";
    
    public static void staticTest(){
        System.out.println("base static: "+s);
    }
}

定義了一個public靜態變量s、一個public實例變量m、一個靜態方法staticTest。

這是子類代碼:

public class Child extends Base {
    public static String s = "child_base";
    public String m = "child";
    
    public static void staticTest(){
        System.out.println("child static: "+s);
    }
}

子類定義了和父類重名的變量和方法。對於一個子類對象,它就有了兩份變量和方法,在子類內部訪問的時候,訪問的是子類的,或者說,子類變量和方法隱藏了父類對應的變量和方法,下面看一下外部訪問的代碼:

public static void main(String[] args) {
    Child c = new Child();
    Base b = c;
    
    System.out.println(b.s);
    System.out.println(b.m);
    b.staticTest();
    
    System.out.println(c.s);
    System.out.println(c.m);
    c.staticTest();
}

以上代碼建立了一個子類對象,而後將對象分別賦值給了子類引用變量c和父類引用變量b,而後經過b和c分別引用變量和方法。這裏須要說明的是,靜態變量和靜態方法通常經過類名直接訪問,但也能夠經過類的對象訪問。程序輸出爲:

static_base
base
base static: static_base
child_base
child
child static: child_base 

當經過b (靜態類型Base) 訪問時,訪問的是Base的變量和方法,當經過c (靜態類型Child)訪問時,訪問的是Child的變量和方法,這稱之爲靜態綁定,即訪問綁定到變量的靜態類型,靜態綁定在程序編譯階段便可決定,而動態綁定則要等到程序運行時。實例變量、靜態變量、靜態方法、private方法,都是靜態綁定的

重載和重寫

重載是指方法名稱相同但參數簽名不一樣(參數個數或類型或順序不一樣),重寫是指子類重寫父類相同參數簽名的方法。對一個函數調用而言,可能有多個匹配的方法,有時候選擇哪個並非那麼明顯,咱們來看個例子:

這裏基類代碼:

public class Base {
    public int sum(int a, int b){
        System.out.println("base_int_int");
        return a+b;
    }
}

它定義了方法sum,下面是子類代碼:

public class Child extends Base {
    public long sum(long a, long b){
        System.out.println("child_long_long");
        return a+b;
    }
}

如下是調用的代碼:

public static void main(String[] args){
    Child c = new Child();
    int a = 2;
    int b = 3;
    c.sum(a, b);
}

這個調用的是哪一個sum方法呢?每一個sum方法都是兼容的,int類型能夠自動轉型爲long,當只有一個方法的時候,那個方法就會被調用。但如今有多個方法可用,子類的sum方法參數類型雖然不徹底匹配可是是兼容的,父類的sum方法參數類型是徹底匹配的。程序輸出爲:

base_int_int

父類類型徹底匹配的方法被調用了。若是父類代碼改爲下面這樣呢?

public class Base {
    public long sum(int a, long b){
        System.out.println("base_int_long");
        return a+b;
    }
}

父類方法類型也不徹底匹配了。程序輸出爲:

base_int_long

調用的仍是父類的方法。父類和子類的兩個方法的類型都不徹底匹配,爲何調用父類的呢?由於父類的更匹配一些。如今修改一會兒類代碼,更改成:

public class Child extends Base {
    public long sum(int a, long b){
        System.out.println("child_int_long");
        return a+b;
    }
}

程序輸出變爲了:

child_int_long

終於調用了子類的方法。能夠看出,當有多個重名函數的時候,在決定要調用哪一個函數的過程當中,首先是按照參數類型進行匹配的,換句話說,尋找在全部重載版本中最匹配的,而後纔看變量的動態類型,進行動態綁定。

父子類型轉換

以前咱們說過,子類型的對象能夠賦值給父類型的引用變量,這叫向上轉型,那父類型的變量能夠賦值給子類型的變量嗎?或者說能夠向下轉型嗎?語法上能夠進行強制類型轉換,但不必定能轉換成功。咱們以上面的例子來示例:

Base b = new Child();
Child c = (Child)b;

Child c = (Child)b就是將變量b的類型強制轉換爲Child並賦值爲c,這是沒有問題的,由於b的動態類型就是Child,但下面代碼是不行的:

Base b = new Base();
Child c = (Child)b;

語法上Java不會報錯,但運行時會拋出錯誤,錯誤爲類型轉換異常。

一個父類的變量,能不能轉換爲一個子類的變量,取決於這個父類變量的動態類型(即引用的對象類型)是否是這個子類或這個子類的子類。

給定一個父類的變量,能不能知道它究竟是不是某個子類的對象,從而安全的進行類型轉換呢?答案是能夠,經過instanceof關鍵字,看下面代碼:

public boolean canCast(Base b){
    return b instanceof Child;
}

這個函數返回Base類型變量是否能夠轉換爲Child類型,instanceof前面是變量,後面是類,返回值是boolean值,表示變量引用的對象是否是該類或其子類的對象。

protected

變量和函數有public/private修飾符,public表示外部能夠訪問,private表示只能內部使用,還有一種可見性介於中間的修飾符protected,表示雖然不能被外部任意訪問,但可被子類訪問。另外,在Java中,protected還表示可被同一個包中的其餘類訪問,無論其餘類是否是該類的子類,後續章節咱們再討論包。

咱們來看個例子,這是基類代碼:

public class Base {
    protected  int currentStep;
    
    protected void step1(){
    }
    
    protected void step2(){        
    }
    
    public void action(){
        this.currentStep = 1;
        step1();
        this.currentStep = 2;
        step2();
    }
}

action() 表示對外提供的行爲,內部有兩個步驟step1()和step2(),使用currentStep變量表示當前進行到了哪一個步驟,step一、step2 和currentStep是protected的,子類通常不重寫action,而只重寫step1和step2,同時,子類能夠直接訪問 currentStep查看進行到了哪一步。子類的代碼是:

public class Child extends Base {
    protected void step1(){
        System.out.println("child step "
                +this.currentStep);
    }
    
    protected void step2(){    
        System.out.println("child step "
                +this.currentStep);
    }
}

使用Child的代碼是:

public static void main(String[] args){
    Child c = new Child();
    c.action();
}

輸出爲:

child step 1
child step 2

基類定義了表示對外行爲的方法action,並定義了能夠被子類重寫的兩個步驟step1和step2,以及被子類查看的變量currentStep,子類經過重寫protected方法step1和step2來修改對外的行爲。

這種思路和設計在設計模式中被稱之爲模板方法,action方法就是一個模板方法,它定義了實現的模板,而具體實現則由子類提供。模板方法在不少框架中有普遍的應用,這是使用protected的一個經常使用場景。關於更多設計模式的內容咱們暫不介紹。

可見性重寫

重寫方法時,通常並不會修改方法的可見性。但咱們仍是要說明一點,重寫時,子類方法不能下降父類方法的可見性,不能下降是指,父類若是是public,則子類也必須是public,父類若是是protected,子類能夠是protected,也能夠是public,即子類能夠升級父類方法的可見性但不能下降。以下所示:

基類代碼爲:

public class Base {
    protected void protect(){
    }
    
    public void open(){        
    }
}

子類代碼爲:

public class Child extends Base {
    //如下是不容許的的,會有編譯錯誤
//    private void protect(){
//    }
    
    //如下是不容許的,會有編譯錯誤
//    protected void open(){        
//    }
    
    public void protect(){        
    }
}

爲何要這樣規定呢?繼承反映的是"is-a"的關係,即子類對象也屬於父類,子類必須支持父類全部對外的行爲,將可見性下降就會減小子類對外的行爲,從而破壞"is-a"的關係,但子類能夠增長父類的行爲,因此提高可見性是沒有問題的。

防止繼承 (final)

上節咱們提到繼承是把雙刃劍,具體緣由咱們後續章節解說,帶來的影響就是,有的時候咱們不但願父類方法被子類重寫,有的時候甚至不但願類被繼承,實現這個的方法就是final關鍵字。以前咱們提過final能夠修飾變量,這是final的另外一個用法。

一個Java類,默認狀況下都是能夠被繼承的,但加了final關鍵字以後就不能被繼承了,以下所示:

public final class Base {
   //....
}

一個非final的類,其中的public/protected實例方法默認狀況下都是能夠被重寫的,但加了final關鍵字後就不能被重寫了,以下所示:

public class Base {
    public final void test(){
        System.out.println("不能被重寫");
    }
} 

小結

本節咱們討論了Java繼承概念引入的一些細節,有些細節可能平時遇到的比較少,但咱們仍是須要對它們有一個比較好的瞭解,包括構造方法的一些細節,變量和方法的重名,父子類型轉換,protected,可見性重寫,final等。

但還有些重要的地方咱們沒有討論,好比,建立子類對象的具體過程?動態綁定是如何實現的?讓咱們下節來探索繼承實現的基本原理。

----------------

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

-----------

更多相關原創文章

計算機程序的思惟邏輯 (13) - 類

計算機程序的思惟邏輯 (14) - 類的組合

計算機程序的思惟邏輯 (15) - 初識繼承和多態

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

計算機程序的思惟邏輯 (18) - 爲何說繼承是把雙刃劍

計算機程序的思惟邏輯 (19) - 接口的本質

計算機程序的思惟邏輯 (20) - 爲何要有抽象類?

計算機程序的思惟邏輯 (21) - 內部類的本質

相關文章
相關標籤/搜索