類再生分為兩種方式:
合成,在新類裡簡單創建原有類的對象。
繼承,它創建一個新類,將其視作現有類的一個“類型”,我們可以原樣采取現有類的形式,並在其中加入新代碼,同時不會對現有類產生影響。
由於這兒涉及到兩個類——基礎類及衍生類,而不再是以前的一個,所以在想象衍生類的結果對象時,可能會產生一些迷惑。從外部看,似乎新類擁有與基礎類相同的接口,而且可包含一些額外的方法和字段。但繼承並非僅僅簡單地復制基礎類的接口瞭事。創建衍生類的一個對象時,它在其中包含瞭基礎類的一個“子對象”。這個子對象就象我們根據基礎類本身創建瞭它的一個對象。從外部看,基礎類的子對象已封裝到衍生類的對象裡瞭。
當然,基礎類子對象應該正確地初始化,而且隻有一種方法能保證這一點:在構建器中執行初始化,通過調
用基礎類構建器,後者有足夠的能力和權限來執行對基礎類的初始化。在衍生類的構建器中,Java 會自動插
入對基礎類構建器的調用。下面這個例子向大傢展示瞭對這種三級繼承的應用:
Java代碼
public class Art {
Art(){
System.out.println("art");
}
}
public class Drawing extends Art {
Drawing(){
System.out.println("drawing");
}
public static void main(String[] args) {
Drawing drawing = new Drawing();
}
}
輸出結果為:
art
drawing
上述例子有自己默認的構建器;也就是說,它們不含任何自變量。編譯器可以很容易地調用它們,因為不存
在具體傳遞什麼自變量的問題。如果類沒有默認的自變量,或者想調用含有一個自變量的某個基礎類構建
器,必須明確地編寫對基礎類的調用代碼。這是用super 關鍵字以及適當的自變量列表實現的,如下所示:
Java代碼
class Game {
Game(int i) {
System.out.println("Game constructor");
}
}
class BoardGame extends Game {
BoardGame(int i) {
super(i);
System.out.println("BoardGame constructor");
}
}
class Chess extends BoardGame {
Chess() {
super(11);
System.out.println("Chess constructor");
}
public static void main(String[] args) {
Chess x = new Chess();
}
}
輸出結果為:
Game constructor
BoardGame constructor
Chess constructor
盡管編譯器會強迫我們對基礎類進行初始化,並要求我們在構建器最開頭做這一工作,但它並不會監視我們
是否正確初始化瞭成員對象。所以對此必須特別加以留意。
繼承的一個好處是它支持“累積開發”,允許我們引入新的代碼,同時不會為現有代碼造成錯誤。這樣可將
新錯誤隔離到新代碼裡。通過從一個現成的、功能性的類繼承,同時增添成員新的數據成員及方法(並重新
定義現有方法),我們可保持現有代碼原封不動(另外有人也許仍在使用它),不會為其引入自己的編程錯
誤。一旦出現錯誤,就知道它肯定是由於自己的新代碼造成的。這樣一來,與修改現有代碼的主體相比,改
正錯誤所需的時間和精力就可以少很多。
繼承最值得註意的地方就是它沒有為新類提供方法。繼承是對新類和基礎類之間的關系的一種表達。可這樣
總結該關系:“新類屬於現有類的一種類型”。
這種表達並不僅僅是對繼承的一種形象化解釋,繼承是直接由語言提供支持的。作為一個例子,大傢可考慮
一個名為Instrument 的基礎類,它用於表示樂器;另一個衍生類叫作Wind。由於繼承意味著基礎類的所有
方法亦可在衍生出來的類中使用,所以我們發給基礎類的任何消息亦可發給衍生類。若Instrument 類有一個
play()方法,則Wind 設備也會有這個方法。這意味著我們能肯定地認為一個Wind 對象也是Instrument的一
種類型。下面這個例子揭示出編譯器如何提供對這一概念的支持:
Java代碼
public class Instrument {
public void play(){
System.out.println("hello");
}
static void tune(Instrument i){
i.play();
}
}
public class Wind extends Instrument {
public static void main(String[] args) {
Wind wind = new Wind();
Instrument.tune(wind);
}
}
運行結果:
hello
這個例子中最有趣的無疑是tune()方法,它能接受一個Instrument句柄。但在Wind.main()中,tune()方法
是通過為其賦予一個Wind 句柄來調用的。由於Java 對類型檢查特別嚴格,所以大傢可能會感到很奇怪,為
什麼接收一種類型的方法也能接收另一種類型呢?但是,我們一定要認識到一個Wind 對象也是一個
Instrument對象。而且對於不在Wind 中的一個Instrument(樂器),沒有方法可以由tune()調用。在
tune()中,代碼適用於Instrument以及從Instrument 衍生出來的任何東西。在這裡,我們將從一個Wind 句
柄轉換成一個Instrument 句柄的行為叫作“上溯造型”。
由於造型的方向是從衍生類到基礎類,箭頭朝上,所以通常把它叫作“上溯造型 ”,即Upcasting。上溯造
型肯定是安全的,因為我們是從一個更特殊的類型到一個更常規的類型。換言之,衍生類是基礎類的一個超
集。它可以包含比基礎類更多的方法,但它至少包含瞭基礎類的方法。進行上溯造型的時候,類接口可能出
現的唯一一個問題是它可能丟失方法,而不是贏得這些方法。這便是在沒有任何明確的造型或者其他特殊標
註的情況下,編譯器為什麼允許上溯造型的原因所在。
繼承中對於final關鍵字的解釋:
final數據
(1) 編譯期常數,它永遠不會改變
(2) 在運行期初始化的一個值,我們不希望它發生變化
Java代碼
final int s = 1;//s為常數
final Object obj = new Object();//obj為不可變的句柄,但是obj內部變量可以變
final int s;//s為常量,但是使用前必須初始化
void method(final int s){}//當調用方法時,s得到一個值,但這個值在方法內隻讀
final方法
之所以要使用final 方法,可能是出於對兩方面理由的考慮。第一個是為方法“上鎖”,防止任何繼承類改變它的本來含義。設計程序時,若希望一個方法的行為在繼承期間保持不變,而且不可被覆蓋或改寫,就可以采取這種做法。
采用final 方法的第二個理由是程序執行的效率。將一個方法設成final 後,編譯器就可以把對那個方法的所有調用都置入“嵌入”調用裡。隻要編譯器發現一個final 方法調用,就會(根據它自己的判斷)忽略為執行方法調用機制而采取的常規代碼插入方法(將自變量壓入堆棧;跳至方法代碼並執行它;跳回來;清除堆棧自變量;最後對返回值進行處理)。相反,它會用方法主體內實際代碼的一個副本來替換方法調用。這樣做可避免方法調用時的系統開銷。當然,若方法體積太大,那麼程序也會變得雍腫,可能受到到不到嵌入代碼所帶來的任何性能提升。因為任何提升都被花在方法內部的時間抵消瞭。Java 編譯器能自動偵測這些情況,並頗為“明智”地決定是否嵌入一個final 方法。然而,最好還是不要完全相信編譯器能正確地作出所有判斷。通常,隻有在方法的代碼量非常少,或者想明確禁止方法被覆蓋的時候,才應考慮將一個方法設為final。
類內所有private方法都自動成為final。由於我們不能訪問一個private方法,所以它絕對不會被其他方法覆蓋(若強行這樣做,編譯器會給出錯誤提示)。可為一個private方法添加final 指示符,但卻不能為那個方法提供任何額外的含義。
final類
將類定義成final後,結果隻是禁止進行繼承——沒有更多的限制。然而,由於它禁止瞭繼承,所以一個final類中的所有方法都默認為final。因為此時再也無法覆蓋它們。所以與我們將一個方法明確聲明為final 一樣,編譯器此時有相同的效率選擇。 可為final 類內的一個方法添加final 指示符,但這樣做沒有任何意義。
類裝載順序:
若基礎類含有另一個基礎類,則另一個基礎類隨即也會載入,以此類推。接下來,會在根基礎類執行static 初始化,再在下一個衍生類執行,以此類推。保證這個順序是非常關鍵的,因為衍生類的初始化可能要依賴於對基礎類成員的正確初始化。
總結:
無論繼承還是合成,我們都可以在現有類型的基礎上創建一個新類型。但在典型情況下,我們通過合成來實現現有類型的“再生”或“重復使用”,將其作為新類型基礎實施過程的一部分使用。但如果想實現接口的“再生”,就應使用繼承。由於衍生或派生出來的類擁有基礎類的接口,所以能夠將其“上溯造型”為基礎類。對於下一章要講述的多形性問題,這一點是至關重要的。
盡管繼承在面向對象的程序設計中得到瞭特別的強調,但在實際啟動一個設計時,最好還是先考慮采用合成技術。隻有在特別必要的時候,才應考慮采用繼承技術(下一章還會講到這個問題)。合成顯得更加靈活。但是,通過對自己的成員類型應用一些繼承技巧,可在運行期準確改變那些成員對象的類型,由此可改變它們的行為。
盡管對於快速項目開發來說,通過合成和繼承實現的代碼再生具有很大的幫助作用。但在允許其他程序員完全依賴它之前,一般都希望能重新設計自己的類結構。我們理想的類結構應該是每個類都有自己特定的用途。它們不能過大(如集成的功能太多,則很難實現它的再生),也不能過小(造成不能由自己使用,或者不能增添新功能)。最終實現的類應該能夠方便地再生。