




版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領
文檔簡介
1、第6章 類再生“Java引人注目的一項特性是代碼的重復使用或者再生。但最具革命意義的是,除代碼的復制和修改以外,我們還能做多得多的其他事情。”在象C那樣的程序化語言里,代碼的重復使用早已可行,但效果不是特別顯著。與Java的其他地方一樣,這個方案解決的也是與類有關的問題。我們通過創建新類來重復使用代碼,但卻用不著重新創建,可以直接使用別人已建好并調試好的現成類。但這樣做必須保證不會干擾原有的代碼。在這一章里,我們將介紹兩個達到這一目標的方法。第一個最簡單:在新類里簡單地創建原有類的對象。我們把這種方法叫作“合成”,因為新類由現有類的對象合并而成。我們只是簡單地重復利用代碼的功能,而不是采用它的
2、形式。第二種方法則顯得稍微有些技巧。它創建一個新類,將其作為現有類的一個“類型”。我們可以原樣采取現有類的形式,并在其中加入新代碼,同時不會對現有的類產生影響。這種魔術般的行為叫作“繼承”(Inheritance),涉及的大多數工作都是由編譯器完成的。對于面向對象的程序設計,“繼承”是最重要的基礎概念之一。它對我們下一章要講述的內容會產生一些額外的影響。對于合成與繼承這兩種方法,大多數語法和行為都是類似的(因為它們都要根據現有的類型生成新類型)。在本章,我們將深入學習這些代碼再生或者重復使用的機制。6.1 合成的語法就以前的學習情況來看,事實上已進行了多次“合成”操作。為進行合成,我們只需在新
3、類里簡單地置入對象句柄即可。舉個例子來說,假定需要在一個對象里容納幾個String對象、兩種基本數據類型以及屬于另一個類的一個對象。對于非基本類型的對象來說,只需將句柄置于新類即可;而對于基本數據類型來說,則需在自己的類中定義它們。如下所示(若執行該程序時有麻煩,請參見第3章3.1.2小節“賦值”):218-219頁程序/: c06:SprinklerSystem.java/ Composition for code reuse.class WaterSource private String s; WaterSource() System.out.println("WaterSou
4、rce()"); s = new String("Constructed"); public String toString() return s; public class SprinklerSystem private String valve1, valve2, valve3, valve4; WaterSource source; int i; float f; void print() System.out.println("valve1 = " + valve1); System.out.println("valve2 =
5、 " + valve2); System.out.println("valve3 = " + valve3); System.out.println("valve4 = " + valve4); System.out.println("i = " + i); System.out.println("f = " + f); System.out.println("source = " + source); public static void main(String args) Spri
6、nklerSystem x = new SprinklerSystem(); x.print(); /:WaterSource內定義的一個方法是比較特別的:toString()。大家不久就會知道,每種非基本類型的對象都有一個toString()方法。若編譯器本來希望一個String,但卻獲得某個這樣的對象,就會調用這個方法。所以在下面這個表達式中:System.out.println("source = " + source) ;編譯器會發現我們試圖向一個WaterSource添加一個String對象("source =")。這對它來說是不可接受的,因為
7、我們只能將一個字串“添加”到另一個字串,所以它會說:“我要調用toString(),把source轉換成字串!”經這樣處理后,它就能編譯兩個字串,并將結果字串傳遞給一個System.out.println()。每次隨同自己創建的一個類允許這種行為的時候,都只需要寫一個toString()方法。如果不深究,可能會草率地認為編譯器會為上述代碼中的每個句柄都自動構造對象(由于Java的安全和謹慎的形象)。例如,可能以為它會為WaterSource調用默認構建器,以便初始化source。打印語句的輸出事實上是:219頁下程序valve1 = nullvalve2 = nullvalve3 = null
8、valve4 = nulli = 0f = 0.0source = null在類內作為字段使用的基本數據會初始化成零,就象第2章指出的那樣。但對象句柄會初始化成null。而且假若試圖為它們中的任何一個調用方法,就會產生一次“違例”。這種結果實際是相當好的(而且很有用),我們可在不丟棄一次違例的前提下,仍然把它們打印出來。編譯器并不只是為每個句柄創建一個默認對象,因為那樣會在許多情況下招致不必要的開銷。如希望句柄得到初始化,可在下面這些地方進行:(1) 在對象定義的時候。這意味著它們在構建器調用之前肯定能得到初始化。(2) 在那個類的構建器中。(3) 緊靠在要求實際使用那個對象之前。這樣做可減少
9、不必要的開銷假如對象并不需要創建的話。下面向大家展示了所有這三種方法:220-221頁程序請注意在Bath構建器中,在所有初始化開始之前執行了一個語句。如果不在定義時進行初始化,仍然不能保證能在將一條消息發給一個對象句柄之前會執行任何初始化除非出現不可避免的運行期違例。下面是該程序的輸出:221頁中程序調用print()時,它會填充s4,使所有字段在使用之前都獲得正確的初始化。6.2 繼承的語法繼承與Java(以及其他OOP語言)非常緊密地結合在一起。我們早在第1章就為大家引入了繼承的概念,并在那章之后到本章之前的各章里不時用到,因為一些特殊的場合要求必須使用繼承。除此以外,創建一個類時肯定會
10、進行繼承,因為若非如此,會從Java的標準根類Object中繼承。用于合成的語法是非常簡單且直觀的。但為了進行繼承,必須采用一種全然不同的形式。需要繼承的時候,我們會說:“這個新類和那個舊類差不多。”為了在代碼里表面這一觀念,需要給出類名。但在類主體的起始花括號之前,需要放置一個關鍵字extends,在后面跟隨“基礎類”的名字。若采取這種做法,就可自動獲得基礎類的所有數據成員以及方法。下面是一個例子:222頁程序這個例子向大家展示了大量特性。首先,在Cleanser append()方法里,字串同一個s連接起來。這是用“+=”運算符實現的。同“+”一樣,“+=”被Java用于對字串進行“過載”
11、處理。其次,無論Cleanser還是Detergent都包含了一個main()方法。我們可為自己的每個類都創建一個main()。通常建議大家象這樣進行編寫代碼,使自己的測試代碼能夠封裝到類內。即便在程序中含有數量眾多的類,但對于在命令行請求的public類,只有main()才會得到調用。所以在這種情況下,當我們使用“java Detergent”的時候,調用的是Degergent.main()即使Cleanser并非一個public類。采用這種將main()置入每個類的做法,可方便地為每個類都進行單元測試。而且在完成測試以后,毋需將main()刪去;可把它保留下來,用于以后的測試。在這里,大家
12、可看到Deteregent.main()對Cleanser.main()的調用是明確進行的。需要著重強調的是Cleanser中的所有類都是public屬性。請記住,倘若省略所有訪問指示符,則成員默認為“友好的”。這樣一來,就只允許對包成員進行訪問。在這個包內,任何人都可使用那些沒有訪問指示符的方法。例如,Detergent將不會遇到任何麻煩。然而,假設來自另外某個包的類準備繼承Cleanser,它就只能訪問那些public成員。所以在計劃繼承的時候,一個比較好的規則是將所有字段都設為private,并將所有方法都設為public(protected成員也允許衍生出來的類訪問它;以后還會深入探討
13、這一問題)。當然,在一些特殊的場合,我們仍然必須作出一些調整,但這并不是一個好的做法。注意Cleanser在它的接口中含有一系列方法:append(),dilute(),apply(),scrub()以及print()。由于Detergent是從Cleanser衍生出來的(通過extends關鍵字),所以它會自動獲得接口內的所有這些方法即使我們在Detergent里并未看到對它們的明確定義。這樣一來,就可將繼承想象成“對接口的重復利用”或者“接口的再生”(以后的實施細節可以自由設置,但那并非我們強調的重點)。正如在scrub()里看到的那樣,可以獲得在基礎類里定義的一個方法,并對其進行修改。在
14、這種情況下,我們通常想在新版本里調用來自基礎類的方法。但在scrub()里,不可只是簡單地發出對scrub()的調用。那樣便造成了遞歸調用,我們不愿看到這一情況。為解決這個問題,Java提供了一個super關鍵字,它引用當前類已從中繼承的一個“超類”(Superclass)。所以表達式super.scrub()調用的是方法scrub()的基礎類版本。進行繼承時,我們并不限于只能使用基礎類的方法。亦可在衍生出來的類里加入自己的新方法。這時采取的做法與在普通類里添加其他任何方法是完全一樣的:只需簡單地定義它即可。extends關鍵字提醒我們準備將新方法加入基礎類的接口里,對其進行“擴展”。foam
15、()便是這種做法的一個產物。在Detergent.main()里,我們可看到對于Detergent對象,可調用Cleanser以及Detergent內所有可用的方法(如foam())。6.2.1 初始化基礎類由于這兒涉及到兩個類基礎類及衍生類,而不再是以前的一個,所以在想象衍生類的結果對象時,可能會產生一些迷惑。從外部看,似乎新類擁有與基礎類相同的接口,而且可包含一些額外的方法和字段。但繼承并非僅僅簡單地復制基礎類的接口了事。創建衍生類的一個對象時,它在其中包含了基礎類的一個“子對象”。這個子對象就象我們根據基礎類本身創建了它的一個對象。從外部看,基礎類的子對象已封裝到衍生類的對象里了。當然,
16、基礎類子對象應該正確地初始化,而且只有一種方法能保證這一點:在構建器中執行初始化,通過調用基礎類構建器,后者有足夠的能力和權限來執行對基礎類的初始化。在衍生類的構建器中,Java會自動插入對基礎類構建器的調用。下面這個例子向大家展示了對這種三級繼承的應用:224-225頁程序該程序的輸出顯示了自動調用:Art constructorDrawing constructorCartoon constructor可以看出,構建是在基礎類的“外部”進行的,所以基礎類會在衍生類訪問它之前得到正確的初始化。即使沒有為Cartoon()創建一個構建器,編譯器也會為我們自動合成一個默認構建器,并發出對基礎類構
17、建器的調用。1. 含有自變量的構建器上述例子有自己默認的構建器;也就是說,它們不含任何自變量。編譯器可以很容易地調用它們,因為不存在具體傳遞什么自變量的問題。如果類沒有默認的自變量,或者想調用含有一個自變量的某個基礎類構建器,必須明確地編寫對基礎類的調用代碼。這是用super關鍵字以及適當的自變量列表實現的,如下所示:225-226頁程序如果不調用BoardGames()內的基礎類構建器,編譯器就會報告自己找不到Games()形式的一個構建器。除此以外,在衍生類構建器中,對基礎類構建器的調用是必須做的第一件事情(如操作失當,編譯器會向我們指出)。2. 捕獲基本構建器的違例正如剛才指出的那樣,編
18、譯器會強迫我們在衍生類構建器的主體中首先設置對基礎類構建器的調用。這意味著在它之前不能出現任何東西。正如大家在第9章會看到的那樣,這同時也會防止衍生類構建器捕獲來自一個基礎類的任何違例事件。顯然,這有時會為我們造成不便。6.3 合成與繼承的結合許多時候都要求將合成與繼承兩種技術結合起來使用。下面這個例子展示了如何同時采用繼承與合成技術,從而創建一個更復雜的類,同時進行必要的構建器初始化工作:226-228頁程序盡管編譯器會強迫我們對基礎類進行初始化,并要求我們在構建器最開頭做這一工作,但它并不會監視我們是否正確初始化了成員對象。所以對此必須特別加以留意。6.3.1 確保正確的清除Java不具備
19、象C+的“破壞器”那樣的概念。在C+中,一旦破壞(清除)一個對象,就會自動調用破壞器方法。之所以將其省略,大概是由于在Java中只需簡單地忘記對象,不需強行破壞它們。垃圾收集器會在必要的時候自動回收內存。垃圾收集器大多數時候都能很好地工作,但在某些情況下,我們的類可能在自己的存在時期采取一些行動,而這些行動要求必須進行明確的清除工作。正如第4章已經指出的那樣,我們并不知道垃圾收集器什么時候才會顯身,或者說不知它何時會調用。所以一旦希望為一個類清除什么東西,必須寫一個特別的方法,明確、專門地來做這件事情。同時,還要讓客戶程序員知道他們必須調用這個方法。而在所有這一切的后面,就如第9章(違例控制)
20、要詳細解釋的那樣,必須將這樣的清除代碼置于一個finally從句中,從而防范任何可能出現的違例事件。下面介紹的是一個計算機輔助設計系統的例子,它能在屏幕上描繪圖形:229-230頁程序這個系統中的所有東西都屬于某種Shape(幾何形狀)。Shape本身是一種Object(對象),因為它是從根類明確繼承的。每個類都重新定義了Shape的cleanup()方法,同時還要用super調用那個方法的基礎類版本。盡管對象存在期間調用的所有方法都可負責做一些要求清除的工作,但對于特定的Shape類Circle(圓)、Triangle(三角形)以及Line(直線),它們都擁有自己的構建器,能完成“作圖”(d
21、raw)任務。每個類都有它們自己的cleanup()方法,用于將非內存的東西恢復回對象存在之前的景象。在main()中,可看到兩個新關鍵字:try和finally。我們要到第9章才會向大家正式引薦它們。其中,try關鍵字指出后面跟隨的塊(由花括號定界)是一個“警戒區”。也就是說,它會受到特別的待遇。其中一種待遇就是:該警戒區后面跟隨的finally從句的代碼肯定會得以執行不管try塊到底存不存在(通過違例控制技術,try塊可有多種不尋常的應用)。在這里,finally從句的意思是“總是為x調用cleanup(),無論會發生什么事情”。這些關鍵字將在第9章進行全面、完整的解釋。在自己的清除方法中
22、,必須注意對基礎類以及成員對象清除方法的調用順序假若一個子對象要以另一個為基礎。通常,應采取與C+編譯器對它的“破壞器”采取的同樣的形式:首先完成與類有關的所有特殊工作(可能要求基礎類元素仍然可見),然后調用基礎類清除方法,就象這兒演示的那樣。許多情況下,清除可能并不是個問題;只需讓垃圾收集器盡它的職責即可。但一旦必須由自己明確清除,就必須特別謹慎,并要求周全的考慮。1. 垃圾收集的順序不能指望自己能確切知道何時會開始垃圾收集。垃圾收集器可能永遠不會得到調用。即使得到調用,它也可能以自己愿意的任何順序回收對象。除此以外,Java 1.0實現的垃圾收集器機制通常不會調用finalize()方法。
23、除內存的回收以外,其他任何東西都最好不要依賴垃圾收集器進行回收。若想明確地清除什么,請制作自己的清除方法,而且不要依賴finalize()。然而正如以前指出的那樣,可強迫Java1.1調用所有收尾模塊(Finalizer)。6.3.2 名字的隱藏只有C+程序員可能才會驚訝于名字的隱藏,因為它的工作原理與在C+里是完全不同的。如果Java基礎類有一個方法名被“過載”使用多次,在衍生類里對那個方法名的重新定義就不會隱藏任何基礎類的版本。所以無論方法在這一級還是在一個基礎類中定義,過載都會生效:232頁程序正如下一章會講到的那樣,很少會用與基礎類里完全一致的簽名和返回類型來覆蓋同名的方法,否則會使人
24、感到迷惑(這正是C+不允許那樣做的原因,所以能夠防止產生一些不必要的錯誤)。6.4 到底選擇合成還是繼承無論合成還是繼承,都允許我們將子對象置于自己的新類中。大家或許會奇怪兩者間的差異,以及到底該如何選擇。如果想利用新類內部一個現有類的特性,而不想使用它的接口,通常應選擇合成。也就是說,我們可嵌入一個對象,使自己能用它實現新類的特性。但新類的用戶會看到我們已定義的接口,而不是來自嵌入對象的接口。考慮到這種效果,我們需在新類里嵌入現有類的private對象。有些時候,我們想讓類用戶直接訪問新類的合成。也就是說,需要將成員對象的屬性變為public。成員對象會將自身隱藏起來,所以這是一種安全的做法
25、。而且在用戶知道我們準備合成一系列組件時,接口就更容易理解。car(汽車)對象便是一個很好的例子:233-234頁程序由于汽車的裝配是故障分析時需要考慮的一項因素(并非只是基礎設計簡單的一部分),所以有助于客戶程序員理解如何使用類,而且類創建者的編程復雜程度也會大幅度降低。如選擇繼承,就需要取得一個現成的類,并制作它的一個特殊版本。通常,這意味著我們準備使用一個常規用途的類,并根據特定的需求對其進行定制。只需稍加想象,就知道自己不能用一個車輛對象來合成一輛汽車汽車并不“包含”車輛;相反,它“屬于”車輛的一種類別。“屬于”關系是用繼承來表達的,而“包含”關系是用合成來表達的。6.5 protec
26、ted現在我們已理解了繼承的概念,protected這個關鍵字最后終于有了意義。在理想情況下,private成員隨時都是“私有”的,任何人不得訪問。但在實際應用中,經常想把某些東西深深地藏起來,但同時允許訪問衍生類的成員。protected關鍵字可幫助我們做到這一點。它的意思是“它本身是私有的,但可由從這個類繼承的任何東西或者同一個包內的其他任何東西訪問”。也就是說,Java中的protected會成為進入“友好”狀態。我們采取的最好的做法是保持成員的private狀態無論如何都應保留對基 礎的實施細節進行修改的權利。在這一前提下,可通過protected方法允許類的繼承者進行受到控制的訪問:
27、235頁程序可以看到,change()擁有對set()的訪問權限,因為它的屬性是protected(受到保護的)。6.6 累積開發繼承的一個好處是它支持“累積開發”,允許我們引入新的代碼,同時不會為現有代碼造成錯誤。這樣可將新錯誤隔離到新代碼里。通過從一個現成的、功能性的類繼承,同時增添成員新的數據成員及方法(并重新定義現有方法),我們可保持現有代碼原封不動(另外有人也許仍在使用它),不會為其引入自己的編程錯誤。一旦出現錯誤,就知道它肯定是由于自己的新代碼造成的。這樣一來,與修改現有代碼的主體相比,改正錯誤所需的時間和精力就可以少很多。類的隔離效果非常好,這是許多程序員事先沒有預料到的。甚至不
28、需要方法的源代碼來實現代碼的再生。最多只需要導入一個包(這對于繼承和合并都是成立的)。大家要記住這樣一個重點:程序開發是一個不斷遞增或者累積的過程,就象人們學習知識一樣。當然可根據要求進行盡可能多的分析,但在一個項目的設計之初,誰都不可能提前獲知所有的答案。如果能將自己的項目看作一個有機的、能不斷進步的生物,從而不斷地發展和改進它,就有望獲得更大的成功以及更直接的反饋。盡管繼承是一種非常有用的技術,但在某些情況下,特別是在項目穩定下來以后,仍然需要從新的角度考察自己的類結構,將其收縮成一個更靈活的結構。請記住,繼承是對一種特殊關系的表達,意味著“這個新類屬于那個舊類的一種類型”。我們的程序不應
29、糾纏于一些細樹末節,而應著眼于創建和操作各種類型的對象,用它們表達出來自“問題空間”的一個模型。6.7 上溯造型繼承最值得注意的地方就是它沒有為新類提供方法。繼承是對新類和基礎類之間的關系的一種表達。可這樣總結該關系:“新類屬于現有類的一種類型”。這種表達并不僅僅是對繼承的一種形象化解釋,繼承是直接由語言提供支持的。作為一個例子,大家可考慮一個名為Instrument的基礎類,它用于表示樂器;另一個衍生類叫作Wind。由于繼承意味著基礎類的所有方法亦可在衍生出來的類中使用,所以我們發給基礎類的任何消息亦可發給衍生類。若Instrument類有一個play()方法,則Wind設備也會有這個方法。
30、這意味著我們能肯定地認為一個Wind對象也是Instrument的一種類型。下面這個例子揭示出編譯器如何提供對這一概念的支持:236-237頁程序這個例子中最有趣的無疑是tune()方法,它能接受一個Instrument句柄。但在Wind.main()中,tune()方法是通過為其賦予一個Wind句柄來調用的。由于Java對類型檢查特別嚴格,所以大家可能會感到很奇怪,為什么接收一種類型的方法也能接收另一種類型呢?但是,我們一定要認識到一個Wind對象也是一個Instrument對象。而且對于不在Wind中的一個Instrument(樂器),沒有方法可以由tune()調用。在tune()中,代碼
31、適用于Instrument以及從Instrument衍生出來的任何東西。在這里,我們將從一個Wind句柄轉換成一個Instrument句柄的行為叫作“上溯造型”。6.7.1 何謂“上溯造型”?之所以叫作這個名字,除了有一定的歷史原因外,也是由于在傳統意義上,類繼承圖的畫法是根位于最頂部,再逐漸向下擴展(當然,可根據自己的習慣用任何方法描繪這種圖)。因素,Wind.java的繼承圖就象下面這個樣子:237頁圖由于造型的方向是從衍生類到基礎類,箭頭朝上,所以通常把它叫作“上溯造型”,即Upcasting。上溯造型肯定是安全的,因為我們是從一個更特殊的類型到一個更常規的類型。換言之,衍生類是基礎類的
32、一個超集。它可以包含比基礎類更多的方法,但它至少包含了基礎類的方法。進行上溯造型的時候,類接口可能出現的唯一一個問題是它可能丟失方法,而不是贏得這些方法。這便是在沒有任何明確的造型或者其他特殊標注的情況下,編譯器為什么允許上溯造型的原因所在。也可以執行下溯造型,但這時會面臨第11章要詳細講述的一種困境。1. 再論合成與繼承在面向對象的程序設計中,創建和使用代碼最可能采取的一種做法是:將數據和方法統一封裝到一個類里,并且使用那個類的對象。有些時候,需通過“合成”技術用現成的類來構造新類。而繼承是最少見的一種做法。因此,盡管繼承在學習OOP的過程中得到了大量的強調,但并不意味著應該盡可能地到處使用
33、它。相反,使用它時要特別慎重。只有在清楚知道繼承在所有方法中最有效的前提下,才可考慮它。為判斷自己到底應該選用合成還是繼承,一個最簡單的辦法就是考慮是否需要從新類上溯造型回基礎類。若必須上溯,就需要繼承。但如果不需要上溯造型,就應提醒自己防止繼承的濫用。在下一章里(多形性),會向大家介紹必須進行上溯造型的一種場合。但只要記住經常問自己“我真的需要上溯造型嗎”,對于合成還是繼承的選擇就不應該是個太大的問題。6.8 final關鍵字由于語境(應用環境)不同,final關鍵字的含義可能會稍微產生一些差異。但它最一般的意思就是聲明“這個東西不能改變”。之所以要禁止改變,可能是考慮到兩方面的因素:設計或
34、效率。由于這兩個原因頗有些區別,所以也許會造成final關鍵字的誤用。在接下去的小節里,我們將討論final關鍵字的三種應用場合:數據、方法以及類。6.8.1 final數據許多程序設計語言都有自己的辦法告訴編譯器某個數據是“常數”。常數主要應用于下述兩個方面:(1) 編譯期常數,它永遠不會改變(2) 在運行期初始化的一個值,我們不希望它發生變化對于編譯期的常數,編譯器(程序)可將常數值“封裝”到需要的計算過程里。也就是說,計算可在編譯期間提前執行,從而節省運行時的一些開銷。在Java中,這些形式的常數必須屬于基本數據類型(Primitives),而且要用final關鍵字進行表達。在對這樣的一
35、個常數進行定義的時候,必須給出一個值。無論static還是final字段,都只能存儲一個數據,而且不得改變。若隨同對象句柄使用final,而不是基本數據類型,它的含義就稍微讓人有點兒迷糊了。對于基本數據類型,final會將值變成一個常數;但對于對象句柄,final會將句柄變成一個常數。進行聲明時,必須將句柄初始化到一個具體的對象。而且永遠不能將句柄變成指向另一個對象。然而,對象本身是可以修改的。Java對此未提供任何手段,可將一個對象直接變成一個常數(但是,我們可自己編寫一個類,使其中的對象具有“常數”效果)。這一限制也適用于數組,它也屬于對象。下面是演示final字段用法的一個例子:29-2
36、40頁程序由于i1和I2都是具有final屬性的基本數據類型,并含有編譯期的值,所以它們除了能作為編譯期的常數使用外,在任何導入方式中也不會出現任何不同。I3是我們體驗此類常數定義時更典型的一種方式:public表示它們可在包外使用;Static強調它們只有一個;而final表明它是一個常數。注意對于含有固定初始化值(即編譯期常數)的fianl static基本數據類型,它們的名字根據規則要全部采用大寫。也要注意i5在編譯期間是未知的,所以它沒有大寫。不能由于某樣東西的屬性是final,就認定它的值能在編譯時期知道。i4和i5向大家證明了這一點。它們在運行期間使用隨機生成的數字。例子的這一部分
37、也向大家揭示出將final值設為static和非static之間的差異。只有當值在運行期間初始化的前提下,這種差異才會揭示出來。因為編譯期間的值被編譯器認為是相同的。這種差異可從輸出結果中看出:240-241頁程序注意對于fd1和fd2來說,i4的值是唯一的,但i5的值不會由于創建了另一個FinalData對象而發生改變。那是因為它的屬性是static,而且在載入時初始化,而非每創建一個對象時初始化。從v1到v4的變量向我們揭示出final句柄的含義。正如大家在main()中看到的那樣,并不能認為由于v2屬于final,所以就不能再改變它的值。然而,我們確實不能再將v2綁定到一個新對象,因為它
38、的屬性是final。這便是final對于一個句柄的確切含義。我們會發現同樣的含義亦適用于數組,后者只不過是另一種類型的句柄而已。將句柄變成final看起來似乎不如將基本數據類型變成final那么有用。2. 空白finalJava 1.1允許我們創建“空白final”,它們屬于一些特殊的字段。盡管被聲明成final,但卻未得到一個初始值。無論在哪種情況下,空白final都必須在實際使用前得到正確的初始化。而且編譯器會主動保證這一規定得以貫徹。然而,對于final關鍵字的各種應用,空白final具有最大的靈活性。舉個例子來說,位于類內部的一個final字段現在對每個對象都可以有所不同,同時依然保持
39、其“不變”的本質。下面列出一個例子:241-242頁程序現在強行要求我們對final進行賦值處理要么在定義字段時使用一個表達 式,要么在每個構建器中。這樣就可以確保final字段在使用前獲得正確的初始化。3. final自變量Java 1.1允許我們將自變量設成final屬性,方法是在自變量列表中對它們進行適當的聲明。這意味著在一個方法的內部,我們不能改變自變量句柄指向的東西。如下所示:242頁程序注意此時仍然能為final自變量分配一個null(空)句柄,同時編譯器不會捕獲它。這與我們對非final自變量采取的操作是一樣的。方法f()和g()向我們展示出基本類型的自變量為final時會發生什
40、么情況:我們只能讀取自變量,不可改變它。6.8.2 final方法之所以要使用final方法,可能是出于對兩方面理由的考慮。第一個是為方法“上鎖”,防止任何繼承類改變它的本來含義。設計程序時,若希望一個方法的行為在繼承期間保持不變,而且不可被覆蓋或改寫,就可以采取這種做法。采用final方法的第二個理由是程序執行的效率。將一個方法設成final后,編譯器就可以把對那個方法的所有調用都置入“嵌入”調用里。只要編譯器發現一個final方法調用,就會(根據它自己的判斷)忽略為執行方法調用機制而采取的常規代碼插入方法(將自變量壓入堆棧;跳至方法代碼并執行它;跳回來;清除堆棧自變量;最后對返回值進行處理
41、)。相反,它會用方法主體內實際代碼的一個副本來替換方法調用。這樣做可避免方法調用時的系統開銷。當然,若方法體積太大,那么程序也會變得雍腫,可能受到到不到嵌入代碼所帶來的任何性能提升。因為任何提升都被花在方法內部的時間抵消了。Java編譯器能自動偵測這些情況,并頗為“明智”地決定是否嵌入一個final方法。然而,最好還是不要完全相信編譯器能正確地作出所有判斷。通常,只有在方法的代碼量非常少,或者想明確禁止方法被覆蓋的時候,才應考慮將一個方法設為final。類內所有private方法都自動成為final。由于我們不能訪問一個private方法,所以它絕對不會被其他方法覆蓋(若強行這樣做,編譯器會給
42、出錯誤提示)。可為一個private方法添加final指示符,但卻不能為那個方法提供任何額外的含義。6.8.3 final類如果說整個類都是final(在它的定義前冠以final關鍵字),就表明自己不希望從這個類繼承,或者不允許其他任何人采取這種操作。換言之,出于這樣或那樣的原因,我們的類肯定不需要進行任何改變;或者出于安全方面的理由,我們不希望進行子類化(子類處理)。除此以外,我們或許還考慮到執行效率的問題,并想確保涉及這個類各對象的所有行動都要盡可能地有效。如下所示:244頁程序注意數據成員既可以是final,也可以不是,取決于我們具體選擇。應用于final的規則同樣適用于數據成員,無論類
43、是否被定義成final。將類定義成final后,結果只是禁止進行繼承沒有更多的限制。然而,由于它禁止了繼承,所以一個final類中的所有方法都默認為final。因為此時再也無法覆蓋它們。所以與我們將一個方法明確聲明為final一樣,編譯器此時有相同的效率選擇。可為final類內的一個方法添加final指示符,但這樣做沒有任何意義。6.8.4 final的注意事項設計一個類時,往往需要考慮是否將一個方法設為final。可能會覺得使用自己的類時執行效率非常重要,沒有人想覆蓋自己的方法。這種想法在某些時候是正確的。但要慎重作出自己的假定。通常,我們很難預測一個類以后會以什么樣的形式再生或重復利用。常
44、規用途的類尤其如此。若將一個方法定義成final,就可能杜絕了在其他程序員的項目中對自己的類進行繼承的途徑,因為我們根本沒有想到它會象那樣使用。標準Java庫是闡述這一觀點的最好例子。其中特別常用的一個類是Vector。如果我們考慮代碼的執行效率,就會發現只有不把任何方法設為final,才能使其發揮更大的作用。我們很容易就會想到自己應繼承和覆蓋如此有用的一個類,但它的設計者卻否定了我們的想法。但我們至少可以用兩個理由來反駁他們。首先,Stack(堆棧)是從Vector繼承來的,亦即Stack“是”一個Vector,這種說法是不確切的。其次,對于Vector許多重要的方法,如addElement
45、()以及elementAt()等,它們都變成了synchronized(同步的)。正如在第14章要講到的那樣,這會造成顯著的性能開銷,可能會把final提供的性能改善抵銷得一干二凈。因此,程序員不得不猜測到底應該在哪里進行優化。在標準庫里居然采用了如此笨拙的設計,真不敢想象會在程序員里引發什么樣的情緒。另一個值得注意的是Hashtable(散列表),它是另一個重要的標準類。該類沒有采用任何final方法。正如我們在本書其他地方提到的那樣,顯然一些類的設計人員與其他設計人員有著全然不同的素質(注意比較Hashtable極短的方法名與Vecor的方法名)。對類庫的用戶來說,這顯然是不應該如此輕易就
46、能看出的。一個產品的設計變得不一致后,會加大用戶的工作量。這也從另一個側面強調了代碼設計與檢查時需要很強的責任心。6.9 初始化和類裝載在許多傳統語言里,程序都是作為啟動過程的一部分一次性載入的。隨后進行的是初始化,再是正式執行程序。在這些語言中,必須對初始化過程進行慎重的控制,保證static數據的初始化不會帶來麻煩。比如在一個static數據獲得初始化之前,就有另一個static數據希望它是一個有效值,那么在C+中就會造成問題。Java則沒有這樣的問題,因為它采用了不同的裝載方法。由于Java中的一切東西都是對象,所以許多活動變得更加簡單,這個問題便是其中的一例。正如下一章會講到的那樣,每個對象的代碼都存在于獨立的文件中。除非真的需要代碼,
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯系上傳者。文件的所有權益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
- 4. 未經權益所有人同意不得將文件中的內容挪作商業或盈利用途。
- 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
- 6. 下載文件中如有侵權或不適當內容,請與我們聯系,我們立即糾正。
- 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 內蒙古自治區根河市市級名校2025年初三高中生物試題競賽模擬(二)生物試題含解析
- 四川省仁壽縣鏵強中學2025屆高三下學期物理試題試卷含解析
- 儀隴縣2025屆數學三下期末聯考試題含解析
- 浙江音樂學院《鍋爐原理B》2023-2024學年第一學期期末試卷
- 四川文化傳媒職業學院《汽車理論A》2023-2024學年第二學期期末試卷
- 重慶輕工職業學院《工程光學設計(雙語)》2023-2024學年第二學期期末試卷
- 七臺河市重點中學2025年學業水平考試英語試題模擬題卷含解析
- 上海立信會計金融學院《醫學免疫學與微生物學》2023-2024學年第二學期期末試卷
- 內蒙古自治區海勃灣區2025年初三下第三次月考化學試題含解析
- 湖南醫藥學院《中醫診斷學技能》2023-2024學年第一學期期末試卷
- 五年級數學下冊期末測試卷及答案【可打印】
- 聯合體施工雙方安全生產協議書范本
- 農行反洗錢與制裁合規知識競賽考試題庫大全-下(判斷題)
- 24.1.4-圓周角-第1課時說課課件-
- (正式版)SH∕T 3507-2024 石油化工鋼結構工程施工及驗收規范
- 土石壩設計計算書
- 重慶市兩江新區2023-2024學年七年級下學期期末考試語文試題
- 中國痔病診療指南(2020版)
- 神經病學(第8版)第六章-周圍神經疾病
- 國際標準《風險管理指南》(ISO31000)的中文版
- 學習興稅-稅收基礎知識考試參考題庫及答案
評論
0/150
提交評論