?? chapter6.htm
字號:
<br>
2. 捕獲基本構建器的違例<br>
正如剛才指出的那樣,編譯器會強迫我們在衍生類構建器的主體中首先設置對基礎類構建器的調用。這意味著在它之前不能出現任何東西。正如大家在第9章會看到的那樣,這同時也會防止衍生類構建器捕獲來自一個基礎類的任何違例事件。顯然,這有時會為我們造成不便。<br>
<br>
6.3 合成與繼承的結合<br>
許多時候都要求將合成與繼承兩種技術結合起來使用。下面這個例子展示了如何同時采用繼承與合成技術,從而創建一個更復雜的類,同時進行必要的構建器初始化工作:<br>
<br>
226-228頁程序<br>
<br>
盡管編譯器會強迫我們對基礎類進行初始化,并要求我們在構建器最開頭做這一工作,但它并不會監視我們是否正確初始化了成員對象。所以對此必須特別加以留意。<br>
<br>
6.3.1 確保正確的清除<br>
Java不具備象C++的“破壞器”那樣的概念。在C++中,一旦破壞(清除)一個對象,就會自動調用破壞器方法。之所以將其省略,大概是由于在Java中只需簡單地忘記對象,不需強行破壞它們。垃圾收集器會在必要的時候自動回收內存。<br>
垃圾收集器大多數時候都能很好地工作,但在某些情況下,我們的類可能在自己的存在時期采取一些行動,而這些行動要求必須進行明確的清除工作。正如第4章已經指出的那樣,我們并不知道垃圾收集器什么時候才會顯身,或者說不知它何時會調用。所以一旦希望為一個類清除什么東西,必須寫一個特別的方法,明確、專門地來做這件事情。同時,還要讓客戶程序員知道他們必須調用這個方法。而在所有這一切的后面,就如第9章(違例控制)要詳細解釋的那樣,必須將這樣的清除代碼置于一個finally從句中,從而防范任何可能出現的違例事件。<br>
下面介紹的是一個計算機輔助設計系統的例子,它能在屏幕上描繪圖形:<br>
<br>
229-230頁程序<br>
<br>
這個系統中的所有東西都屬于某種Shape(幾何形狀)。Shape本身是一種Object(對象),因為它是從根類明確繼承的。每個類都重新定義了Shape的cleanup()方法,同時還要用super調用那個方法的基礎類版本。盡管對象存在期間調用的所有方法都可負責做一些要求清除的工作,但對于特定的Shape類——Circle(圓)、Triangle(三角形)以及Line(直線),它們都擁有自己的構建器,能完成“作圖”(draw)任務。每個類都有它們自己的cleanup()方法,用于將非內存的東西恢復回對象存在之前的景象。<br>
在main()中,可看到兩個新關鍵字:try和finally。我們要到第9章才會向大家正式引薦它們。其中,try關鍵字指出后面跟隨的塊(由花括號定界)是一個“警戒區”。也就是說,它會受到特別的待遇。其中一種待遇就是:該警戒區后面跟隨的finally從句的代碼肯定會得以執行——不管try塊到底存不存在(通過違例控制技術,try塊可有多種不尋常的應用)。在這里,finally從句的意思是“總是為x調用cleanup(),無論會發生什么事情”。這些關鍵字將在第9章進行全面、完整的解釋。<br>
在自己的清除方法中,必須注意對基礎類以及成員對象清除方法的調用順序——假若一個子對象要以另一個為基礎。通常,應采取與C++編譯器對它的“破壞器”采取的同樣的形式:首先完成與類有關的所有特殊工作(可能要求基礎類元素仍然可見),然后調用基礎類清除方法,就象這兒演示的那樣。<br>
許多情況下,清除可能并不是個問題;只需讓垃圾收集器盡它的職責即可。但一旦必須由自己明確清除,就必須特別謹慎,并要求周全的考慮。<br>
<br>
1. 垃圾收集的順序<br>
不能指望自己能確切知道何時會開始垃圾收集。垃圾收集器可能永遠不會得到調用。即使得到調用,它也可能以自己愿意的任何順序回收對象。除此以外,Java
1.0實現的垃圾收集器機制通常不會調用finalize()方法。除內存的回收以外,其他任何東西都最好不要依賴垃圾收集器進行回收。若想明確地清除什么,請制作自己的清除方法,而且不要依賴finalize()。然而正如以前指出的那樣,可強迫Java1.1調用所有收尾模塊(Finalizer)。<br>
<br>
6.3.2 名字的隱藏<br>
只有C++程序員可能才會驚訝于名字的隱藏,因為它的工作原理與在C++里是完全不同的。如果Java基礎類有一個方法名被“過載”使用多次,在衍生類里對那個方法名的重新定義就不會隱藏任何基礎類的版本。所以無論方法在這一級還是在一個基礎類中定義,過載都會生效:<br>
<br>
232頁程序<br>
<br>
正如下一章會講到的那樣,很少會用與基礎類里完全一致的簽名和返回類型來覆蓋同名的方法,否則會使人感到迷惑(這正是C++不允許那樣做的原因,所以能夠防止產生一些不必要的錯誤)。<br>
<br>
6.4 到底選擇合成還是繼承<br>
無論合成還是繼承,都允許我們將子對象置于自己的新類中。大家或許會奇怪兩者間的差異,以及到底該如何選擇。<br>
如果想利用新類內部一個現有類的特性,而不想使用它的接口,通常應選擇合成。也就是說,我們可嵌入一個對象,使自己能用它實現新類的特性。但新類的用戶會看到我們已定義的接口,而不是來自嵌入對象的接口。考慮到這種效果,我們需在新類里嵌入現有類的private對象。<br>
有些時候,我們想讓類用戶直接訪問新類的合成。也就是說,需要將成員對象的屬性變為public。成員對象會將自身隱藏起來,所以這是一種安全的做法。而且在用戶知道我們準備合成一系列組件時,接口就更容易理解。car(汽車)對象便是一個很好的例子:<br>
<br>
233-234頁程序<br>
<br>
由于汽車的裝配是故障分析時需要考慮的一項因素(并非只是基礎設計簡單的一部分),所以有助于客戶程序員理解如何使用類,而且類創建者的編程復雜程度也會大幅度降低。<br>
如選擇繼承,就需要取得一個現成的類,并制作它的一個特殊版本。通常,這意味著我們準備使用一個常規用途的類,并根據特定的需求對其進行定制。只需稍加想象,就知道自己不能用一個車輛對象來合成一輛汽車——汽車并不“包含”車輛;相反,它“屬于”車輛的一種類別。“屬于”關系是用繼承來表達的,而“包含”關系是用合成來表達的。<br>
<br>
6.5 protected<br>
現在我們已理解了繼承的概念,protected這個關鍵字最后終于有了意義。在理想情況下,private成員隨時都是“私有”的,任何人不得訪問。但在實際應用中,經常想把某些東西深深地藏起來,但同時允許訪問衍生類的成員。protected關鍵字可幫助我們做到這一點。它的意思是“它本身是私有的,但可由從這個類繼承的任何東西或者同一個包內的其他任何東西訪問”。也就是說,Java中的protected會成為進入“友好”狀態。<br>
我們采取的最好的做法是保持成員的private狀態——無論如何都應保留對基
礎的實施細節進行修改的權利。在這一前提下,可通過protected方法允許類的繼承者進行受到控制的訪問:<br>
<br>
235頁程序<br>
<br>
可以看到,change()擁有對set()的訪問權限,因為它的屬性是protected(受到保護的)。<br>
<br>
6.6 累積開發<br>
繼承的一個好處是它支持“累積開發”,允許我們引入新的代碼,同時不會為現有代碼造成錯誤。這樣可將新錯誤隔離到新代碼里。通過從一個現成的、功能性的類繼承,同時增添成員新的數據成員及方法(并重新定義現有方法),我們可保持現有代碼原封不動(另外有人也許仍在使用它),不會為其引入自己的編程錯誤。一旦出現錯誤,就知道它肯定是由于自己的新代碼造成的。這樣一來,與修改現有代碼的主體相比,改正錯誤所需的時間和精力就可以少很多。<br>
類的隔離效果非常好,這是許多程序員事先沒有預料到的。甚至不需要方法的源代碼來實現代碼的再生。最多只需要導入一個包(這對于繼承和合并都是成立的)。<br>
大家要記住這樣一個重點:程序開發是一個不斷遞增或者累積的過程,就象人們學習知識一樣。當然可根據要求進行盡可能多的分析,但在一個項目的設計之初,誰都不可能提前獲知所有的答案。如果能將自己的項目看作一個有機的、能不斷進步的生物,從而不斷地發展和改進它,就有望獲得更大的成功以及更直接的反饋。<br>
盡管繼承是一種非常有用的技術,但在某些情況下,特別是在項目穩定下來以后,仍然需要從新的角度考察自己的類結構,將其收縮成一個更靈活的結構。請記住,繼承是對一種特殊關系的表達,意味著“這個新類屬于那個舊類的一種類型”。我們的程序不應糾纏于一些細樹末節,而應著眼于創建和操作各種類型的對象,用它們表達出來自“問題空間”的一個模型。<br>
<br>
6.7 上溯造型<br>
繼承最值得注意的地方就是它沒有為新類提供方法。繼承是對新類和基礎類之間的關系的一種表達。可這樣總結該關系:“新類屬于現有類的一種類型”。<br>
這種表達并不僅僅是對繼承的一種形象化解釋,繼承是直接由語言提供支持的。作為一個例子,大家可考慮一個名為Instrument的基礎類,它用于表示樂器;另一個衍生類叫作Wind。由于繼承意味著基礎類的所有方法亦可在衍生出來的類中使用,所以我們發給基礎類的任何消息亦可發給衍生類。若Instrument類有一個play()方法,則Wind設備也會有這個方法。這意味著我們能肯定地認為一個Wind對象也是Instrument的一種類型。下面這個例子揭示出編譯器如何提供對這一概念的支持:<br>
<br>
236-237頁程序<br>
<br>
這個例子中最有趣的無疑是tune()方法,它能接受一個Instrument句柄。但在Wind.main()中,tune()方法是通過為其賦予一個Wind句柄來調用的。由于Java對類型檢查特別嚴格,所以大家可能會感到很奇怪,為什么接收一種類型的方法也能接收另一種類型呢?但是,我們一定要認識到一個Wind對象也是一個Instrument對象。而且對于不在Wind中的一個Instrument(樂器),沒有方法可以由tune()調用。在tune()中,代碼適用于Instrument以及從Instrument衍生出來的任何東西。在這里,我們將從一個Wind句柄轉換成一個Instrument句柄的行為叫作“上溯造型”。<br>
<br>
6.7.1 何謂“上溯造型”?<br>
之所以叫作這個名字,除了有一定的歷史原因外,也是由于在傳統意義上,類繼承圖的畫法是根位于最頂部,再逐漸向下擴展(當然,可根據自己的習慣用任何方法描繪這種圖)。因素,Wind.java的繼承圖就象下面這個樣子:<br>
237頁圖<br>
<br>
由于造型的方向是從衍生類到基礎類,箭頭朝上,所以通常把它叫作“上溯造型”,即Upcasting。上溯造型肯定是安全的,因為我們是從一個更特殊的類型到一個更常規的類型。換言之,衍生類是基礎類的一個超集。它可以包含比基礎類更多的方法,但它至少包含了基礎類的方法。進行上溯造型的時候,類接口可能出現的唯一一個問題是它可能丟失方法,而不是贏得這些方法。這便是在沒有任何明確的造型或者其他特殊標注的情況下,編譯器為什么允許上溯造型的原因所在。<br>
也可以執行下溯造型,但這時會面臨第11章要詳細講述的一種困境。<br>
<br>
1. 再論合成與繼承<br>
在面向對象的程序設計中,創建和使用代碼最可能采取的一種做法是:將數據和方法統一封裝到一個類里,并且使用那個類的對象。有些時候,需通過“合成”技術用現成的類來構造新類。而繼承是最少見的一種做法。因此,盡管繼承在學習OOP的過程中得到了大量的強調,但并不意味著應該盡可能地到處使用它。相反,使用它時要特別慎重。只有在清楚知道繼承在所有方法中最有效的前提下,才可考慮它。為判斷自己到底應該選用合成還是繼承,一個最簡單的辦法就是考慮是否需要從新類上溯造型回基礎類。若必須上溯,就需要繼承。但如果不需要上溯造型,就應提醒自己防止繼承的濫用。在下一章里(多形性),會向大家介紹必須進行上溯造型的一種場合。但只要記住經常問自己“我真的需要上溯造型嗎”,對于合成還是繼承的選擇就不應該是個太大的問題。<br>
<br>
6.8 final關鍵字<br>
由于語境(應用環境)不同,final關鍵字的含義可能會稍微產生一些差異。但它最一般的意思就是聲明“這個東西不能改變”。之所以要禁止改變,可能是考慮到兩方面的因素:設計或效率。由于這兩個原因頗有些區別,所以也許會造成final關鍵字的誤用。<br>
在接下去的小節里,我們將討論final關鍵字的三種應用場合:數據、方法以及類。<br>
<br>
6.8.1 final數據<br>
許多程序設計語言都有自己的辦法告訴編譯器某個數據是“常數”。常數主要應用于下述兩個方面:<br>
(1) 編譯期常數,它永遠不會改變<br>
(2) 在運行期初始化的一個值,我們不希望它發生變化<br>
對于編譯期的常數,編譯器(程序)可將常數值“封裝”到需要的計算過程里。也就是說,計算可在編譯期間提前執行,從而節省運行時的一些開銷。在Java中,這些形式的常數必須屬于基本數據類型(Primitives),而且要用final關鍵字進行表達。在對這樣的一個常數進行定義的時候,必須給出一個值。<br>
無論static還是final字段,都只能存儲一個數據,而且不得改變。<br>
若隨同對象句柄使用final,而不是基本數據類型,它的含義就稍微讓人有點兒迷糊了。對于基本數據類型,final會將值變成一個常數;但對于對象句柄,final會將句柄變成一個常數。進行聲明時,必須將句柄初始化到一個具體的對象。而且永遠不能將句柄變成指向另一個對象。然而,對象本身是可以修改的。Java對此未提供任何手段,可將一個對象直接變成一個常數(但是,我們可自己編寫一個類,使其中的對象具有“常數”效果)。這一限制也適用于數組,它也屬于對象。<br>
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -