Java編程思想:第七章:複用類

第七章:複用類


    複用代碼是Java衆多引入注目的功能之一。但要想成爲極具革命性的語言,僅僅能夠複製代碼並對之加以改變是不夠的,它還必須能夠做更多的事情。

    組合和繼承而言,其語法和行爲大多是相似的。由於它們是利用現有類型生成新類型,所以這樣做極富意義。


7.1 組合語法

    組合語法,只需要將對象引用置於新類中即可。

    每一個非基本類型的對象都有一個toString()方法,而且當編譯器需要一個String而你卻只有一個對象時,該方法便會被調用。

    編譯器並不簡單的爲每一個引用都創建默認對象,這一點是很有意義的,因爲若真要那樣做的話,就會在許多情況下增加不必要的負擔。如果想初始化這些引用,可以在代碼中的下列位置進行:

    • 在定義對象的地方。這意味着它們總是在構造器被調用之前得到初始化。

    • 在類的構造器中。

    • 就在正要使用這些對象之前。

    • 使用實例初始化


7.2 繼承語法

    在Java中,當創建一個類時,總是在繼承,因此,除非以明確指出要從其他類中繼承,否則就是在隱式地從Java的標準跟類Object進行繼承。

    繼承使用的是一種特殊的語法。在繼承過程中,需要先聲明“新類與舊類相似”。這種聲明是通過在類主體的左邊花括號之前,書寫後面緊隨基類名稱的關鍵字extends而實現的。當這麼做,會自動得到基類的所有方法和域。如:class xxxx extends yyyy。

    在繼承的過程中,並不一定非得使用基類的方法。也可以在子類中添加新的方法,其添加方式與在類中添加任意方法一樣。


初始化基類

    由於現在涉及基類和子類,而不是隻有一個類,所以要試着想像子類所產生的結果對象,會有點困惑。從外部來看,它就像是一個與基類相同接口的新類。或許還會有一些額外的方法或域。但繼承並不只是複製基類的接口。當創建一個子類對象的時候,該對象包含了一個基類的子對象,這個子對象與你用基類直接創建的對象是一樣的。二者區別在於,後者來自於外部,而基類的子對象被包裝在子類對象內部。

    當然,對基類子對象的準確初始化也是至關重要的,而且也僅有一種方法來保證這一點:在構造器中調用基類構造器來執行初始化,而基類構造器具有執行基類初始化所需要的知識和能力。Java在自動在子類的構造器中插入對基類構造器的調用。

    構造過程是從基類“向外”擴散的,所以基類在子類構造器可以訪問它之前,就已經完成了初始化。


帶參數的構造器

    如果沒有默認的基類構造器,或者想調用一個帶參數的基類構造器,就必須用關鍵字super顯示地編寫調用基類構造器的語句,並且配以適當的參數列表。而且super必須放到構造器的第一行。


7.3 代理

    第三種關係稱爲代理,Java沒有提供直接的支持。這是繼承與組合之間的關係。因爲我們將一個成員對象置於所要構造的類中(就像組合),但與此同時我們在新類中暴露了改成員對象的所有方法(就像繼承)。因此代理解決了此問題。

    我們使用代理可以擁有更多的控制權,因爲我們可以選擇只提供在成員對象中的方法的某個子集。

    儘管Java中不直接支持代理,但是很多開發工具支持代理。


7.4 結合使用組合和繼承

    同時使用組合和繼承是很常見的。

    雖然編譯器強制去你初始化基類,並且要求你要在構造器起始就要這麼做,但是它並不監督你必須將成員對象也初始化,因此在這一點上你自己必須時刻注意。


確保準確清理

    在Java中垃圾回收器負責銷燬對象。但有時類可能要在其生命週期內執行一些必須的清理活動。你根本不知道垃圾回收器何時被調用,或者它是否已經被調用。因此,如果你想要某個類清理一些東西,就必須顯示的編寫一個特殊方法來做這個事,並要確保客戶端程序員知曉他們必須要調用這個方法。

    在清理方法中,還必須注意對基類清理方法和成員對象清理方法的調動順序,以防某個子對象依賴於另一個子對象情形的發生。首先,執行類的所有特定的清理動作,其順序同生成順序相反;然而,就如我們所師範的那樣,調用基類的清理方法。

    許多情況下,清理並不是問題,僅需要讓垃圾回收器完成該動作就行。但當必須親自處理清理時,謬得多做努力並多加小心。因此,一旦涉及垃圾回收,能夠信賴的事就不會很多了。垃圾回收器可能永遠也不會被調用,即使被調用,它也可能以任何它想要的順序來回收對象。最好的辦法就是除了內存以外,不能依賴垃圾回收器去做任何事情。如果需要進行清理,最好是編寫自己的清理方法。但不要使用finalize()方法。


名稱屏蔽

    如果Java的基類擁有某個以被多次重載的方法名稱,那麼在子類中重新定義該方法的名稱並不會屏蔽其在基類中的任何版本,因此,無論是改層或者它的基類中對方法進行定義,重載機制都可以正常工作。

    JavaSE5增加了@Override註解,它並不是關鍵字,但是可以把它當做關鍵字使用。當你需要覆寫某個方法時,就添加這個註解。在你不留心重載而並非覆寫了該方法時,編譯器就會生成一條錯誤信息。


7.5 在組合與繼承之間的選擇

    組合和繼承都允許在新的類中放置子對象,組合是顯示低這樣做,而繼承則是隱式地做。

    組合技術通常用於想在新類中使用現有類的功能而非它的接口這種情形。即,在新類中嵌入某個對象,讓其實現所需要的功能,但新類的用戶看到的只是爲新類所定義的接口,而非所嵌入對象的接口。爲取得此效果,需要在新類中嵌入一個現有類的private對象。

    有時,允許類的用戶直接訪問新類中的組合成分是極具有意義的。也就是說,將成員對象聲明爲public。如果成員對象自身都隱藏了具體實現,那麼這種做法是安全的。當用戶能夠瞭解到你正在組裝部件時,會使得端口更加易於理解。

    在繼承的時候,使用某個現有類,並開發一個它的特殊版本。通常,這意味着你在使用一個通用類,併爲了某種特殊需要而將其特殊化。比如:用一個“交通工具”對象來構建一部"車子“是毫無意義的,因爲”車子“並不包含”交通工具“,它僅是一種交通工具(is-a),這個關係是用繼承來表示的,而has-a關係則是用組合來表示的。


7.6 protected關鍵字

    在實際項目中,經常會想要某些事物儘可能對這個世界隱藏起來,但仍然允許子類的成員訪問它們。

    關鍵字protected就起這個作用。它指明”就類用戶而言,這是private,但對於任何繼承與此類的子類或其他任何位於同一個包內的類來說,它卻是可以訪問“。

    儘管可以創建protected域,但是最好的方式還是將域保持爲private。你應當一直保留“更改底層實現”的權利。然後通過protected方法來控制類的繼承者的訪問權限。


7.7 向上轉型

    爲“新的類型提供方法”並不是繼承技術中最重要的方面,其最重要的方面是用來標示新類和基類之間的關係。這種關係可以由“新類是現有類的一種類型”加以概括。

    這個描述並非只是一種解析繼承的華麗的方式,這直接是由語言所支撐的。

    Java對類型的檢測是非常嚴格的,接收某種類型的方法同樣可以接收另外一種類型就顯得很奇怪。除非你認識到子類也是一種基類對象。這種子類引用轉換爲基類引用,稱爲向上轉換。


爲什麼稱爲向上轉換

    歷史原因,並且是傳統的繼承圖的繪製法爲基礎的:將根植於頁面的頂部,然後逐漸向下,如下:

image.png

    由子類型轉基類型,在繼承圖上是先上移動的,因此一般稱爲:向上轉型。由於向上轉型是一個專用類型向通用類型轉換的,所以總是很安全的。也就是說,子類是基類的一個超集。它可能比基類含有更多的方法,但它必須至少具備基類中所含有的方法。在向上轉型的過程中,類接口中唯一可能發生的事情是丟失方法,而不是獲得它們。這就是爲什麼編譯器在“未曾明確表示轉型”或“未曾指定特殊標記”的情況下,仍然允許向上轉型的原因。


再論組合和繼承

    到底是改用結合或繼承?

    • 問一問自己是否需要從新類向基類進行向上轉型,如果必須向上轉型,則繼承是必要的。但如果不需要,則應當好好考慮自己是否需要繼承。


7.8 final關鍵字

    根據上下文環境,Java的關鍵字final的含義存在細微的區別,別通常它指的是“這是無法改變的”,不想做改變可能處於兩種原因:

    • 設計

    • 效率

    由於這兩個願意相差很遠,所以final有可能被誤用。


final數據

    許多編程語言都有某種方法,來向編譯器告知一塊數據時永恆不變的。有時數據的永恆不變很有用的,比如:

    • 一個永不改變的編譯時常量。

    • 一個在運行時被初始化的值,而你不希望它被改變。

    對於編譯器常量這種情況,編譯器可以將該常量值代入任何可能用到它的計算式中,也就是說,可以在編譯時執行計算式,這減輕了一些運行時的負擔。在Java中,這類常量必須是基本數據類型,並且以關鍵字final表示。在對這個常量進行定義的時候,必須對其進行賦值。

    一個即使static又是final的域只佔據一段不能改變的存儲空間。

    當對對象引用而不是基本類型運用final時,其含義會有一點令人迷惑。對於基本類型,final使數值永恆不變。而用於對象引用,final使引用永恆不變。一旦引用被初始化指向一個對象,就無法再把它改爲指向另一個對象。然而,對象其自身卻是可以被修改的,Java並未提供使任何對象永恆不變的途徑。這一限制同樣用於數組,它也是對象。

    我們不能因爲某數據時final的就認爲在編譯器可以知道它的值。看起來,使引用稱爲final沒有使基本類型稱爲final的用處大。


空白final

    Java允許生成“空白final”,所謂空白final指被聲明爲final但有未給定初識的值的域。無論什麼情況,編譯器都確保空白final在使用前必須初始化。但是,空白final在關鍵字final的使用上提供了更大的靈活性,因此,一個類中final域就可以做到根據對象而有所不同,卻又保持其永恆不變的特性。

    必須在域定義處或每個構造器中用表達式對final進行賦值,這正是final域在使用前總是被初始化的原因所在。


final參數

    Java允許在參數列表中以聲明的方式將參數指明爲final,這意味着你無法在方法中更改參數引用所指向的對象。你可以讀參數,但卻無法修改參數。這一特性主要用於向匿名內部類傳遞參數。


final方法

    使用final方法的原因有兩個:

    • 把方法鎖定,以防止任何繼承類修改它的含義。這是出於設計的考慮。想要確保在繼承中使方法行爲不變,並且不會被覆蓋。

    • 效率,在Java的早期實現中,如果將一個方法聲明爲final,就是同意編譯器將針對該方法的所有調用都轉爲內嵌調用。當編譯器發現一個final方法調用命令時,它會根據自己的判斷,跳過插入程序代碼這種正常方式而執行方法調用機制,並且以方法體中的實際代碼的副本來替代方法調用。這將消除方法調用的開銷。當然,一個方法很大,你的程序代碼就會膨脹,因爲可能看不到內嵌帶來的任何性能提高,因爲,所帶來的性能提高會因爲花費與方法內的時間量而被縮減。

    在最近的Java版本中,虛擬機(特別是hotspot)可以檢測到這種情況,並優化去掉這小效率反而降低額外的內嵌調用,因此不再需要引用final方法來進行優化了。


final與private關鍵字

    類中所有的private方法都是隱式的指定爲final的。由於無法取用private方法,所以也就無法覆蓋它。可以對private方法添加final,但這並不能給該方法增加任何額外的意義。這問題可能造成混亂。因此,如果你試圖覆蓋一個private方法(隱含是final),似乎是有效的,而且編譯器也不會出錯誤信息的。因爲這個private方法就是該子類的private方法來對待了,和基類的對應的方法沒有任何關係了,所以編譯器允許這麼做。


final類

    當類定義爲final時,就表明你不打算繼承該類,而且也不允許別人這樣做。由於filan類禁止繼承,所以final類中的所有方法都隱式指定爲final的,因爲無法覆蓋他們。

    在final類中可以給方法添加final修飾符,但這不會添加任何意義。


有關final的忠告

    在類設計時,把方法聲明爲final是比較明智的。而且這就防止其他使用該類的程序員了,他們根本不能覆蓋次方法。所以使用時考慮場景。


7.9 初始化及類的加載

    Java採用不同的加載方式,以爲Java中所有都是對象。請記住,每一個類的編譯代碼都存在於它自己的獨立的文件中。該文件只在需要使用程序代碼時纔會被加載。一般來說,可以說“類的代碼在初始化使用時才加載”。這通常是值加載發生於創建類的第一個實例對象之前,但是當訪問static域或者方法時,也會發生加載。

    初次使用之處也是static初始化發生之處。所有的static對象和static代碼段都會在加載時依程序中的順序而依次初始化。當然,定義爲static的東西只會被初始化一次。


繼承與初始化

    瞭解繼承在內的初始化過程,以對所發生的的一切有個全局性的把握,是很有意義的。

    如果編譯器注意到它有一個基類,於是它就繼續進行加載。不管你是否打算產生一個基類的對象,這都要發生。如果該基類也有基類,那麼第二個基類也會被加載,如此類推。接下來,根基類中的static初始化即會被執行,然後是下一個子類,依次類推。這種方式很重要,因爲子類的static初始化可能會依賴於基類成員是否準確初始化。

    到此爲止,必要的類都已加載完畢,對象就可以創建了。首先,對象中的所有基本類型都設值爲默認值,對象引用設值爲null:這是通過將對象內存設爲二進制值來完成的。然後,基類的構造器會被調用,在基類構造器完成之後,實例變量初始化。最後構造器的區域部分被執行。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章