2025-05-24

 

類再生分為兩種方式:

合成,在新類裡簡單創建原有類的對象。

繼承,它創建一個新類,將其視作現有類的一個“類型”,我們可以原樣采取現有類的形式,並在其中加入新代碼,同時不會對現有類產生影響。

由於這兒涉及到兩個類——基礎類及衍生類,而不再是以前的一個,所以在想象衍生類的結果對象時,可能會產生一些迷惑。從外部看,似乎新類擁有與基礎類相同的接口,而且可包含一些額外的方法和字段。但繼承並非僅僅簡單地復制基礎類的接口瞭事。創建衍生類的一個對象時,它在其中包含瞭基礎類的一個“子對象”。這個子對象就象我們根據基礎類本身創建瞭它的一個對象。從外部看,基礎類的子對象已封裝到衍生類的對象裡瞭。

當然,基礎類子對象應該正確地初始化,而且隻有一種方法能保證這一點:在構建器中執行初始化,通過調

用基礎類構建器,後者有足夠的能力和權限來執行對基礎類的初始化。在衍生類的構建器中,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 初始化,再在下一個衍生類執行,以此類推。保證這個順序是非常關鍵的,因為衍生類的初始化可能要依賴於對基礎類成員的正確初始化。

 

總結:

無論繼承還是合成,我們都可以在現有類型的基礎上創建一個新類型。但在典型情況下,我們通過合成來實現現有類型的“再生”或“重復使用”,將其作為新類型基礎實施過程的一部分使用。但如果想實現接口的“再生”,就應使用繼承。由於衍生或派生出來的類擁有基礎類的接口,所以能夠將其“上溯造型”為基礎類。對於下一章要講述的多形性問題,這一點是至關重要的。

盡管繼承在面向對象的程序設計中得到瞭特別的強調,但在實際啟動一個設計時,最好還是先考慮采用合成技術。隻有在特別必要的時候,才應考慮采用繼承技術(下一章還會講到這個問題)。合成顯得更加靈活。但是,通過對自己的成員類型應用一些繼承技巧,可在運行期準確改變那些成員對象的類型,由此可改變它們的行為。

盡管對於快速項目開發來說,通過合成和繼承實現的代碼再生具有很大的幫助作用。但在允許其他程序員完全依賴它之前,一般都希望能重新設計自己的類結構。我們理想的類結構應該是每個類都有自己特定的用途。它們不能過大(如集成的功能太多,則很難實現它的再生),也不能過小(造成不能由自己使用,或者不能增添新功能)。最終實現的類應該能夠方便地再生。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *