上節我們介紹了繼承和多態的基本概念,基本概念是比較簡單的,子類繼承父類,自動擁有父類的屬性和行為,並可擴展屬性和行為,同時,可重寫父類的方法以修改行為。
但繼承和多態概念還有一些相關的細節,本節就來探討這些細節,具體包括:
下面我們逐個來解釋。
構造方法
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); } }
構造方法調用重寫方法
如果在父類構造方法中調用了可被重寫的方法,則可能會出現意想不到的結果,我們來看個例子:
這是基類代碼:
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變量表示當前進行到了哪個步驟,step1、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編程及計算機技術的本質。原創文章,保留所有版權。