第五章 繼承
繼承是指基於已有的類構造一個新類,繼承已有類就是複用(繼承)這些類的成員變量和方法。並在此基礎上,添加新的成員變量和方法,以知足新的需求。java不支持多繼承。java
5.1 類、超類和子類
5.1.1 定義子類
下面是由繼承Employee類來定義Manager類的格式,關鍵字extend
表示繼承。數組
public class Manager extend Employee{ //添加方法和成員變量 }
extend
代表定義的新類派生於一個已有類。被繼承的類稱爲超類、基類或父類;新類稱爲子類或派生類。ide
在Manager類中增長了一個用於存儲獎金信息的域,以及一個用於設置這個域的新方法:this
public class Manager extend Employee{ private double bonus; ... public void setBonus(double bonus){ this.bonus = bonus; } }
儘管在Manager類中沒有顯式的定義getName
和getHireDay
等方法,但Manager類的對象卻可使用它們,這是由於Manager類自動繼承了超類Employee中的這些方法。此外,Manager類還從父類繼承了name、salary
和hireDay
這3個成員變量。從而每個Manager對象包含四個成員變量。spa
5.1.2 覆蓋方法(Override)
子類除了能夠定義新方法外,還能夠覆蓋(重寫)父類的方法。覆蓋要遵循 "兩同兩小一大"的原則:設計
兩同指:code
- 方法名相同。
- 形參列表相同,即形參個數相同,各形參類型必須對應相同(新方法的形參類型不能夠是父類方法形參類型的子類型)。
兩小指:對象
- 子類方法返回值類型應與父類方法返回值類型相同或是父類方法返回值類型的子類型。
- 子類方法聲明拋出的異常應與父類方法聲明拋出的異常類型相同或是其子異常。
一大指:子類方法的訪問權限應與父類方法的訪問權限相同或比父類訪問權限大。繼承
今後處能夠看出,覆蓋和重載的區別:接口
方法重載對返回值類型和修飾符沒有要求;實例方法能夠重載父類的類方法,而方法覆蓋要求返回值類型變小,修飾符變大且只能爲實例方法,知足上述條件的靜態方法,不叫覆蓋,只是屏蔽。
尤爲要指出的是:知足覆蓋條件的方法要麼都是類方法,要麼都是實例方法,不能一個是類方法,一個是實例方法,不然會引起編譯錯誤。
編譯時,編譯器會爲每一個類創建一張方法表,表中包含的方法有:子類中定義的全部方法(包括私有方法,構造器,類方法和公有方法)。同時包含從父類繼承的非私有方法,包含重載的方法,應該也包含類方法,但不包含被子類覆蓋的方法。此外,編譯器也會爲每一個對象分配一個成員變量表,其中包含本類中定義的全部變量(成員變量和類變量),以及父類中非私有的變量。
當子類覆蓋了父類方法後,子類對象將沒法訪問父類中被覆蓋的方法,但子類方法中仍能調用父類中被覆蓋的方法,調用格式爲:
- "super.方法名" (被覆蓋的是實例方法),僅能夠在子類非靜態方法使用。編譯時,編譯器會根據super去查找父類的方法表,並替換爲匹配方法的符號引用。
- "父類類名.方法名"
public class Manager extend Employee{ private double bonus; ... public double getSalary(){ return super.getSalary() + bonus; } }
若是父類方法的訪問權限是private
,則子類沒法訪問該方法,且沒法覆蓋該方法,當子類中定義了一個與父類 private
方法知足上述」兩同兩小一大「原則的方法時,並無覆蓋該方法,而是在子類中定義了一個新方法。例如:
class BaseClass{ //test()方法是private訪問權限,子類不可訪問 private void test(){...} } class SubClass extends BaseClass{ //此處不是覆蓋,因此能夠增長static關鍵字 public static void test(){...} }
從而,子類能夠繼承的內容有
- 父類的成員變量(對於父類的私有成員變量,子類對象依然分配有內存,僅由於子類對象不能直接訪問)。
- 父類的非私有方法。
不能繼承的有:
- 父類中被覆蓋的方法,不能繼承。
- 父類的私有方法。
5.1.3 super
限定
super用於限定該對象調用它從父類繼承獲得的實例變量或方法。super不能出如今static
修飾的方法中。
若是在構造器中使用super
,則super
用於初始化從父類繼承的成員變量。
對於非私有域而言,若是子類定義了和父類同名的實例變量,則會發生子類實例變量隱藏父類實例變量的情形。默認狀況下,子類裏定義的方法直接訪問的同名實例變量是子類的實例變量。但能夠在使用"super.實例變量名"來訪問被隱藏的父類的實例變量。例如:
class BaseClass{ public int a; } class SubClass extends BaseClass{ public int a = 7; public void test1(){ System.out.println(a); } public void test2(){ System.out.println(super.a); } }
若是子類中沒有與父類同名的成員變量,那麼在子類實例方法中訪問該成員變量時,無須顯式使用super
或父類名。若是某個方法中訪問了名爲a
的實例變量,但沒有顯式指定調用者,則系統查找a
的順序爲:
- 查找該方法中是否有名爲
a
的局部變量 - 查找當前類是否有名爲
a
的成員變量 - 查找直接父類中是否包含了名爲
a
的成員變量,依次上溯全部父類,直至Object
類,若是沒有找到,則報錯。
當程序建立一個子類對象時,系統不只會爲子類中定義的實例變量分配內存,也會爲從父類繼承的全部實例變量分配內存,即便子類中定義了與父類中同名的實例變量。
因爲子類的實例變量僅是隱藏了父類中同名的實例變量,不是覆蓋。因此,訪問哪一個實例變量是由調用者的類型決定的。在編譯時,由編譯器分派。從而,會出現以下情形:
class Parent{ String tag = "parent"; } class Child extends Parent{ private String tag = "child"; } public class Test{ public static void main(String[] args){ Child c = new Child(); //報錯,不可訪問私有變量 out.println(c.tag);///////1 //輸出:parent out.println(((Parent)c).tag);//////2 } }
當程序在代碼1處試圖訪問tag
時,因爲調用者爲子類,而子類的tag
是私有變量,不能在外部被訪問.。而代碼2處訪問的是父類的tag
。此與方法的覆蓋不一樣,方法覆蓋具備多態性。
綜上,當子類中隱藏了父類的實例變量,或子類中覆蓋了父類的方法時,父類被隱藏的實例變量,或被覆蓋的方法,僅能在子類的方法裏面經過super
來訪問,在其餘類的方法中沒法訪問。
5.1.4 子類構造器
因爲子類不能訪問父類的私有域,因此須要利用父類的構造器來初始化這部分私有域,能夠經過super實現對父類構造器的調用。使用super調用構造器的語句必須是子類構造器的第一條語句。
不論是否使用super
顯式的調用了父類構造器,子類構造器總會調用一次父類構造器,子類調用父類構造器分以下幾種狀況:
- 子類構造器執行體的第一行使用
super
顯式調用了父類構造器,系統將根據super
調用傳入的實參列表調用父類構造器。 - 子類構造器執行體的第一行代碼使用
this
顯式調用本類中重載的其餘構造器,系統將根據this
調用裏傳入的實參列表調用本類的另外一個構造器。執行本類中另外一個構造器時即會調用父類構造器。 - 子類構造器執行體中既沒有使用
super
調用,也沒有使用this
調用,系統將會在執行子類構造器以前,隱式調用父類的無參數構造器。(因此,定義類時,最好提供一個無參數構造器)。
綜上,調用子類構造器時,父類構造器總會在子類構造器以前執行。以此類推,執行父類構造器時,系統會再次上溯執行其父類的構造器.....從而,建立任何java
對象,最早執行的老是Object
類的構造器。當父類有構造器,但不存在默認構造器時,程序出錯。
實際上初始化塊是一個假象,源文件編譯時,初始化塊中的代碼會被"還原"到每一個構造器中,且位於構造器全部代碼的前面。(super()調用的後面,this調用的前面??)。
在建立子類對象時,各模塊的初始化流程以下:
- 在未執行任何初始化語句時,系統已經爲子類的全部成員變量,以及父類的全部成員變量分配了內存空間,並默認初始化,當子類中存在成員變量隱藏父類成員變量狀況時,這兩個成員變量都會被分配內存。
- 而後初始化父類(先執行初始化語句和初始化塊,再執行父類構造器,若子類構造器中有
super
調用父類構造器語句,則調用指定的父類構造器,不然調用父類的默認構造器。 - 按序執行子類的顯式初始化語句或初始化塊。
- 執行子類構造器內的語句。
示例以下:
public class Test{ { a = 6; } int a = 9; public static void main(String[] args){ //輸出爲9;若調換兩個初始化語句,輸出爲6 out.println(a); } }
在首次使用子類時,類的初始化流程爲:
若父類未初始化,則執行父類的類初始化,在執行父類初始化時,也要先判斷其父類是否已初始化,若未初始化,則先執行其父類的初始化,……直至Object類,最後才執行本類的類初始化。
5.2 多態
Java
中引用變量有兩個類型:編譯時類型和運行時類型。編譯時類型由變量定義的類型決定,運行時類型由該變量引用對象的類型決定。若是編譯時類型和運行時類型不一致,就可能出現多態(PolyMophism
)。
由於子類是一種特殊的父類,因此Java
容許把一個子類對象直接賦給一個父類變量,由系統自動完成,無須類型轉換,稱爲向上轉型。但不能將父類對象直接賦給子類變量。
兩個同類型的變量,若一個引用父類對象,另外一個引用的是子類對象,且子類中覆蓋了父類的方法,那麼兩個變量同時調用此方法時,將呈現出不一樣的行爲特徵,這被稱爲多態。
當父類變量引用子類對象時,因爲變量的編譯時類型爲父類,因此只能調用父類的方法和成員變量,以及子類中覆蓋的方法(動態鏈接、動態綁定),不能調用子類中新定義的、以及父類中存在,但子類重載後的只屬於子類方法。符號引用在編譯時分派。
與方法不一樣的是,對象的實例變量不具有多態性。老是訪問編譯時類型中定義的實例變量。
在繼承鏈中對象方法的調用存在一個優先級:????
this.show(O),super.show(O),this.show((super)O),super.show((super)O)
警告,在java
中,子類數組的引用能夠賦給父類數組的引用,而不須要強制類型轉換。例如:
Manager[] managers = new Manager[10]; //將它賦給Employee[] 數組是徹底合法的: Employee[] staff = managers;
由於managers[i]是一個Manager,能夠賦給Employee變量,可是編譯器不容許讓數組元素再引用其餘類型的對象,不容許數組元素引用的類型不一致。以下面語句將會拋出ArrayStoreException
異常:
staff[0] = new Employee(...);
由於若是容許staff[0] 和 managers[0] 都引用了這個Employee 對象,那麼managers[0].getBonus()
變的不合理。
即數組元素只能引用相同類型的對象,不然編譯器會報告異常。
理解初始化流程和多態性的一個極好的例子:
public class Mytest { public static void main(String[] args){ Dervied td = new Dervied(); td.test(); } } class Dervied extends Base { private String name = "dervied"; public Dervied() { super(); tellName(); printName(); } public void tellName() { System.out.println("Dervied tell name: " + name); } public void printName() { System.out.println("Dervied print name: " + name); } } class Base { private String name = "base"; public Base() { tellName(); printName(); } public void tellName() { System.out.println("Base tell name: " + name); } public void printName() { System.out.println("Base print name: " + name); } public void test(){ tellName(); } } 輸出結果爲: //前兩句輸出,說明Base類中的tellName()和printName()仍然調用的是Dervied類的方法 //同時也說明,初始化流程爲:先調用父構造器,而不是先執行顯式初始化語句:private String name = "dervied"; Dervied tell name: null Dervied print name: null //3、四兩句說明調用完Base類構造器後,初始化流程爲:先執行顯式初始化語句,而後再執行構造器中的語句。 Dervied tell name: dervied Dervied print name: dervied //最後一句再次驗證了,Base類中的tellName()和printName()已經完全被覆蓋 Dervied tell name: dervied
5.3 理解方法調用
假設要調用x.f(args)
,下面是調用過程的詳細描述:
-
編譯器查看對象的編譯時類型類型和方法名。假設隱式參數 x 的編譯時類型爲 C 類。須要注意的是:有可能存在多個名字爲 f,可是形參列表不一樣的方法。例如可能存在方法
f(int)
和方法f(String)
。編譯器從方法表中列舉出全部方法名爲f
的方法,包括父類中非私有的且名爲 f 的方法。至此,編譯器已得到全部可能被調用的候選方法。 -
接下來,編譯器將查看調用方法時提供的實參類型。若是在全部名爲 f 的方法中存在一個與提供的參數類型徹底匹配,就選擇這個方法。這個過程被稱爲重載解析(overloading resolution)。因爲容許類型轉換(int 能夠轉換成 double,子類轉換成父類,等等),因此過程很複雜,若是編譯器沒有找到與參數類型匹配的方法,或者發現通過類型轉換後有多個方法與之匹配,就會報錯。至此,編譯器已肯定須要調用的方法,會將其替換爲對應方法的符號引用。
5.4 阻止繼承:final 類和方法
不容許繼承的類被稱爲 final 類。在定義類時使用 final 修飾符,就代表這個類是 final 類,不能被繼承,聲明格式以下:
public final class Executive{ ... }
類中特定的方法也能夠被聲明爲 final。被 final 修飾的非私有方法不能被子類覆蓋,可是方法仍然能夠被重載(由於重載不考慮修飾符)。子類中能夠定義父類中被final
修飾的私有方法。
域也能夠被聲明爲 final,final 域代表域爲常量。當一個類被聲明爲 final 時,只是其中的方法變爲 final,不包括域。
5.5 強制類型轉換
引用變量只能調用編譯時類型中的方法,不能調用運行時類型中定義的方法。若是須要調用運行時類型中的方法,必須進行類型轉換,將它強制轉換成運行時類型。引用類型轉換的語法和基本類型的強制轉換相同。可是,若是父類變量實際類型是超類時,強制類型轉換會引起ClassCastException
。所以,能夠在進行類型轉換以前,先使用instanceof
檢測是否能進行轉換:
if(staff[1] instanceof Manager){ boss = = (Manager) staff[1]; ... }
若是檢測返回false,編譯器就不會進行轉換。
綜上所述:
- 只能在繼承層次內進行類型轉換。
- 在將超類轉換成子類以前,應該使用
instanceof
進行檢查。
只有在使用子類特有的方法時才須要進行類型轉換。建議儘可能少用到類型轉換和 instaceof
運算符。
5.6 instanceOf
運算符
instanceOf
運算符的前一個操做符一般是一個引用類型變量,後一個操做符是一個類或接口;instanceOf
用於判斷前面引用變量的運行時類型是不是後面類或者其子類的實例。若是是,則返回true
,不然返回false
。
使用
instanceOf
運算符時,要求前面的操做數的編譯時類型與後面操做數的類型相同,或者前者是後者的父類,或者前者是後者的子類。不然會引發編譯錯誤。
5.7 繼承與組合
5.7.1 使用繼承的注意點
容許子類繼承父類,子類能夠直接訪問父類的成員變量和方法。可是繼承破壞了父類的封裝性:子類能夠經過覆蓋的方式改變父類方法的實現,從而致使子類能夠惡意篡改父類的方法。而且,父類方法調用被覆蓋方法時,調用的實際上是子類的方法。
爲了保證父類良好的封裝性,設計父類一般應該遵循以下規則:
- 儘可能隱藏父類的內部數據,儘可能把父類的全部成員變量設置爲
private
。 - 不要讓子類能夠隨意訪問、修改父類的方法。父類中做爲輔助的方法應設置爲
private
。父類中須要被外部類調用的方法,必須設置爲public
,若是不但願子類重寫該方法,能夠用final
修飾;若是但願父類的某個方法被子類重寫,但不但願被其餘類自由訪問,則可使用protected
來修飾。 - 儘可能不要在父類構造器中調用將被子類重寫的方法,容易出現邏輯混亂。
5.7.2 組合
組合是將待複用的類當成另外一個類的一部分,即在新類中,定義一個待複用類的私有成員變量,以實現對類方法和成員變量的複用。例如:
class Animal{ public void breath(){ System.out.println("吸氣,吐氣。。。"); } } class Bird{ private Animal a; public void breath(){ a.breath(); } }
組合和繼承均可以複用指定類的方法以及成員變量。繼承更符合現實意義。組合能夠避免破壞父類的封裝性。
5.8 受保護的訪問
若是但願超類中的某些方法容許被子類訪問,或容許子類的方法訪問超類的某個域,此時,須要將這些方法或域聲明爲 protected
。
可是,將父類的域聲明爲 protected後,子類的方法只能訪問子類對象的 protected 域,不能訪問父類對象的 protected 域。這種限制有助於避免濫用受保護機制,使得子類只能得到受保護域的權利。
概括 java
用於修飾成員變量或方法的控制可見性的4個訪問修飾符:
- 僅對本類可見 -- private
- 對全部類可見 -- public
- 對本包和全部子類可見 -- protected
- 僅對本包可見 -- 默認,不添加修飾符
類的可見性:
- 對全部類可見 --
public
- 僅對本包可見 -- 默認,無修飾符。
- 對於內部類而言,本包和子類可見 -- protected
- 對於內部類而言,本類可見 --
private