Java引人注目的一項(xiàng)特性是代碼的重復(fù)使用或者再生但最_第1頁
Java引人注目的一項(xiàng)特性是代碼的重復(fù)使用或者再生但最_第2頁
Java引人注目的一項(xiàng)特性是代碼的重復(fù)使用或者再生但最_第3頁
Java引人注目的一項(xiàng)特性是代碼的重復(fù)使用或者再生但最_第4頁
Java引人注目的一項(xiàng)特性是代碼的重復(fù)使用或者再生但最_第5頁
已閱讀5頁,還剩9頁未讀 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)

文檔簡(jiǎn)介

1、第6章 類再生“Java引人注目的一項(xiàng)特性是代碼的重復(fù)使用或者再生。但最具革命意義的是,除代碼的復(fù)制和修改以外,我們還能做多得多的其他事情。”在象C那樣的程序化語言里,代碼的重復(fù)使用早已可行,但效果不是特別顯著。與Java的其他地方一樣,這個(gè)方案解決的也是與類有關(guān)的問題。我們通過創(chuàng)建新類來重復(fù)使用代碼,但卻用不著重新創(chuàng)建,可以直接使用別人已建好并調(diào)試好的現(xiàn)成類。但這樣做必須保證不會(huì)干擾原有的代碼。在這一章里,我們將介紹兩個(gè)達(dá)到這一目標(biāo)的方法。第一個(gè)最簡(jiǎn)單:在新類里簡(jiǎn)單地創(chuàng)建原有類的對(duì)象。我們把這種方法叫作“合成”,因?yàn)樾骂愑涩F(xiàn)有類的對(duì)象合并而成。我們只是簡(jiǎn)單地重復(fù)利用代碼的功能,而不是采用它的

2、形式。第二種方法則顯得稍微有些技巧。它創(chuàng)建一個(gè)新類,將其作為現(xiàn)有類的一個(gè)“類型”。我們可以原樣采取現(xiàn)有類的形式,并在其中加入新代碼,同時(shí)不會(huì)對(duì)現(xiàn)有的類產(chǎn)生影響。這種魔術(shù)般的行為叫作“繼承”(Inheritance),涉及的大多數(shù)工作都是由編譯器完成的。對(duì)于面向?qū)ο蟮某绦蛟O(shè)計(jì),“繼承”是最重要的基礎(chǔ)概念之一。它對(duì)我們下一章要講述的內(nèi)容會(huì)產(chǎn)生一些額外的影響。對(duì)于合成與繼承這兩種方法,大多數(shù)語法和行為都是類似的(因?yàn)樗鼈兌家鶕?jù)現(xiàn)有的類型生成新類型)。在本章,我們將深入學(xué)習(xí)這些代碼再生或者重復(fù)使用的機(jī)制。6.1 合成的語法就以前的學(xué)習(xí)情況來看,事實(shí)上已進(jìn)行了多次“合成”操作。為進(jìn)行合成,我們只需在新

3、類里簡(jiǎn)單地置入對(duì)象句柄即可。舉個(gè)例子來說,假定需要在一個(gè)對(duì)象里容納幾個(gè)String對(duì)象、兩種基本數(shù)據(jù)類型以及屬于另一個(gè)類的一個(gè)對(duì)象。對(duì)于非基本類型的對(duì)象來說,只需將句柄置于新類即可;而對(duì)于基本數(shù)據(jù)類型來說,則需在自己的類中定義它們。如下所示(若執(zhí)行該程序時(shí)有麻煩,請(qǐng)參見第3章3.1.2小節(jié)“賦值”):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內(nèi)定義的一個(gè)方法是比較特別的:toString()。大家不久就會(huì)知道,每種非基本類型的對(duì)象都有一個(gè)toString()方法。若編譯器本來希望一個(gè)String,但卻獲得某個(gè)這樣的對(duì)象,就會(huì)調(diào)用這個(gè)方法。所以在下面這個(gè)表達(dá)式中:System.out.println("source = " + source) ;編譯器會(huì)發(fā)現(xiàn)我們?cè)噲D向一個(gè)WaterSource添加一個(gè)String對(duì)象("source =")。這對(duì)它來說是不可接受的,因?yàn)?/p>

7、我們只能將一個(gè)字串“添加”到另一個(gè)字串,所以它會(huì)說:“我要調(diào)用toString(),把source轉(zhuǎn)換成字串!”經(jīng)這樣處理后,它就能編譯兩個(gè)字串,并將結(jié)果字串傳遞給一個(gè)System.out.println()。每次隨同自己創(chuàng)建的一個(gè)類允許這種行為的時(shí)候,都只需要寫一個(gè)toString()方法。如果不深究,可能會(huì)草率地認(rèn)為編譯器會(huì)為上述代碼中的每個(gè)句柄都自動(dòng)構(gòu)造對(duì)象(由于Java的安全和謹(jǐn)慎的形象)。例如,可能以為它會(huì)為WaterSource調(diào)用默認(rèn)構(gòu)建器,以便初始化source。打印語句的輸出事實(shí)上是:219頁下程序valve1 = nullvalve2 = nullvalve3 = null

8、valve4 = nulli = 0f = 0.0source = null在類內(nèi)作為字段使用的基本數(shù)據(jù)會(huì)初始化成零,就象第2章指出的那樣。但對(duì)象句柄會(huì)初始化成null。而且假若試圖為它們中的任何一個(gè)調(diào)用方法,就會(huì)產(chǎn)生一次“違例”。這種結(jié)果實(shí)際是相當(dāng)好的(而且很有用),我們可在不丟棄一次違例的前提下,仍然把它們打印出來。編譯器并不只是為每個(gè)句柄創(chuàng)建一個(gè)默認(rèn)對(duì)象,因?yàn)槟菢訒?huì)在許多情況下招致不必要的開銷。如希望句柄得到初始化,可在下面這些地方進(jìn)行:(1) 在對(duì)象定義的時(shí)候。這意味著它們?cè)跇?gòu)建器調(diào)用之前肯定能得到初始化。(2) 在那個(gè)類的構(gòu)建器中。(3) 緊靠在要求實(shí)際使用那個(gè)對(duì)象之前。這樣做可減少

9、不必要的開銷假如對(duì)象并不需要?jiǎng)?chuàng)建的話。下面向大家展示了所有這三種方法:220-221頁程序請(qǐng)注意在Bath構(gòu)建器中,在所有初始化開始之前執(zhí)行了一個(gè)語句。如果不在定義時(shí)進(jìn)行初始化,仍然不能保證能在將一條消息發(fā)給一個(gè)對(duì)象句柄之前會(huì)執(zhí)行任何初始化除非出現(xiàn)不可避免的運(yùn)行期違例。下面是該程序的輸出:221頁中程序調(diào)用print()時(shí),它會(huì)填充s4,使所有字段在使用之前都獲得正確的初始化。6.2 繼承的語法繼承與Java(以及其他OOP語言)非常緊密地結(jié)合在一起。我們?cè)缭诘?章就為大家引入了繼承的概念,并在那章之后到本章之前的各章里不時(shí)用到,因?yàn)橐恍┨厥獾膱?chǎng)合要求必須使用繼承。除此以外,創(chuàng)建一個(gè)類時(shí)肯定會(huì)

10、進(jìn)行繼承,因?yàn)槿舴侨绱耍瑫?huì)從Java的標(biāo)準(zhǔn)根類Object中繼承。用于合成的語法是非常簡(jiǎn)單且直觀的。但為了進(jìn)行繼承,必須采用一種全然不同的形式。需要繼承的時(shí)候,我們會(huì)說:“這個(gè)新類和那個(gè)舊類差不多。”為了在代碼里表面這一觀念,需要給出類名。但在類主體的起始花括號(hào)之前,需要放置一個(gè)關(guān)鍵字extends,在后面跟隨“基礎(chǔ)類”的名字。若采取這種做法,就可自動(dòng)獲得基礎(chǔ)類的所有數(shù)據(jù)成員以及方法。下面是一個(gè)例子:222頁程序這個(gè)例子向大家展示了大量特性。首先,在Cleanser append()方法里,字串同一個(gè)s連接起來。這是用“+=”運(yùn)算符實(shí)現(xiàn)的。同“+”一樣,“+=”被Java用于對(duì)字串進(jìn)行“過載”

11、處理。其次,無論Cleanser還是Detergent都包含了一個(gè)main()方法。我們可為自己的每個(gè)類都創(chuàng)建一個(gè)main()。通常建議大家象這樣進(jìn)行編寫代碼,使自己的測(cè)試代碼能夠封裝到類內(nèi)。即便在程序中含有數(shù)量眾多的類,但對(duì)于在命令行請(qǐng)求的public類,只有main()才會(huì)得到調(diào)用。所以在這種情況下,當(dāng)我們使用“java Detergent”的時(shí)候,調(diào)用的是Degergent.main()即使Cleanser并非一個(gè)public類。采用這種將main()置入每個(gè)類的做法,可方便地為每個(gè)類都進(jìn)行單元測(cè)試。而且在完成測(cè)試以后,毋需將main()刪去;可把它保留下來,用于以后的測(cè)試。在這里,大家

12、可看到Deteregent.main()對(duì)Cleanser.main()的調(diào)用是明確進(jìn)行的。需要著重強(qiáng)調(diào)的是Cleanser中的所有類都是public屬性。請(qǐng)記住,倘若省略所有訪問指示符,則成員默認(rèn)為“友好的”。這樣一來,就只允許對(duì)包成員進(jìn)行訪問。在這個(gè)包內(nèi),任何人都可使用那些沒有訪問指示符的方法。例如,Detergent將不會(huì)遇到任何麻煩。然而,假設(shè)來自另外某個(gè)包的類準(zhǔn)備繼承Cleanser,它就只能訪問那些public成員。所以在計(jì)劃繼承的時(shí)候,一個(gè)比較好的規(guī)則是將所有字段都設(shè)為private,并將所有方法都設(shè)為public(protected成員也允許衍生出來的類訪問它;以后還會(huì)深入探討

13、這一問題)。當(dāng)然,在一些特殊的場(chǎng)合,我們?nèi)匀槐仨氉鞒鲆恍┱{(diào)整,但這并不是一個(gè)好的做法。注意Cleanser在它的接口中含有一系列方法:append(),dilute(),apply(),scrub()以及print()。由于Detergent是從Cleanser衍生出來的(通過extends關(guān)鍵字),所以它會(huì)自動(dòng)獲得接口內(nèi)的所有這些方法即使我們?cè)贒etergent里并未看到對(duì)它們的明確定義。這樣一來,就可將繼承想象成“對(duì)接口的重復(fù)利用”或者“接口的再生”(以后的實(shí)施細(xì)節(jié)可以自由設(shè)置,但那并非我們強(qiáng)調(diào)的重點(diǎn))。正如在scrub()里看到的那樣,可以獲得在基礎(chǔ)類里定義的一個(gè)方法,并對(duì)其進(jìn)行修改。在

14、這種情況下,我們通常想在新版本里調(diào)用來自基礎(chǔ)類的方法。但在scrub()里,不可只是簡(jiǎn)單地發(fā)出對(duì)scrub()的調(diào)用。那樣便造成了遞歸調(diào)用,我們不愿看到這一情況。為解決這個(gè)問題,Java提供了一個(gè)super關(guān)鍵字,它引用當(dāng)前類已從中繼承的一個(gè)“超類”(Superclass)。所以表達(dá)式super.scrub()調(diào)用的是方法scrub()的基礎(chǔ)類版本。進(jìn)行繼承時(shí),我們并不限于只能使用基礎(chǔ)類的方法。亦可在衍生出來的類里加入自己的新方法。這時(shí)采取的做法與在普通類里添加其他任何方法是完全一樣的:只需簡(jiǎn)單地定義它即可。extends關(guān)鍵字提醒我們準(zhǔn)備將新方法加入基礎(chǔ)類的接口里,對(duì)其進(jìn)行“擴(kuò)展”。foam

15、()便是這種做法的一個(gè)產(chǎn)物。在Detergent.main()里,我們可看到對(duì)于Detergent對(duì)象,可調(diào)用Cleanser以及Detergent內(nèi)所有可用的方法(如foam())。6.2.1 初始化基礎(chǔ)類由于這兒涉及到兩個(gè)類基礎(chǔ)類及衍生類,而不再是以前的一個(gè),所以在想象衍生類的結(jié)果對(duì)象時(shí),可能會(huì)產(chǎn)生一些迷惑。從外部看,似乎新類擁有與基礎(chǔ)類相同的接口,而且可包含一些額外的方法和字段。但繼承并非僅僅簡(jiǎn)單地復(fù)制基礎(chǔ)類的接口了事。創(chuàng)建衍生類的一個(gè)對(duì)象時(shí),它在其中包含了基礎(chǔ)類的一個(gè)“子對(duì)象”。這個(gè)子對(duì)象就象我們根據(jù)基礎(chǔ)類本身創(chuàng)建了它的一個(gè)對(duì)象。從外部看,基礎(chǔ)類的子對(duì)象已封裝到衍生類的對(duì)象里了。當(dāng)然,

16、基礎(chǔ)類子對(duì)象應(yīng)該正確地初始化,而且只有一種方法能保證這一點(diǎn):在構(gòu)建器中執(zhí)行初始化,通過調(diào)用基礎(chǔ)類構(gòu)建器,后者有足夠的能力和權(quán)限來執(zhí)行對(duì)基礎(chǔ)類的初始化。在衍生類的構(gòu)建器中,Java會(huì)自動(dòng)插入對(duì)基礎(chǔ)類構(gòu)建器的調(diào)用。下面這個(gè)例子向大家展示了對(duì)這種三級(jí)繼承的應(yīng)用:224-225頁程序該程序的輸出顯示了自動(dòng)調(diào)用:Art constructorDrawing constructorCartoon constructor可以看出,構(gòu)建是在基礎(chǔ)類的“外部”進(jìn)行的,所以基礎(chǔ)類會(huì)在衍生類訪問它之前得到正確的初始化。即使沒有為Cartoon()創(chuàng)建一個(gè)構(gòu)建器,編譯器也會(huì)為我們自動(dòng)合成一個(gè)默認(rèn)構(gòu)建器,并發(fā)出對(duì)基礎(chǔ)類構(gòu)

17、建器的調(diào)用。1. 含有自變量的構(gòu)建器上述例子有自己默認(rèn)的構(gòu)建器;也就是說,它們不含任何自變量。編譯器可以很容易地調(diào)用它們,因?yàn)椴淮嬖诰唧w傳遞什么自變量的問題。如果類沒有默認(rèn)的自變量,或者想調(diào)用含有一個(gè)自變量的某個(gè)基礎(chǔ)類構(gòu)建器,必須明確地編寫對(duì)基礎(chǔ)類的調(diào)用代碼。這是用super關(guān)鍵字以及適當(dāng)?shù)淖宰兞苛斜韺?shí)現(xiàn)的,如下所示:225-226頁程序如果不調(diào)用BoardGames()內(nèi)的基礎(chǔ)類構(gòu)建器,編譯器就會(huì)報(bào)告自己找不到Games()形式的一個(gè)構(gòu)建器。除此以外,在衍生類構(gòu)建器中,對(duì)基礎(chǔ)類構(gòu)建器的調(diào)用是必須做的第一件事情(如操作失當(dāng),編譯器會(huì)向我們指出)。2. 捕獲基本構(gòu)建器的違例正如剛才指出的那樣,編

18、譯器會(huì)強(qiáng)迫我們?cè)谘苌悩?gòu)建器的主體中首先設(shè)置對(duì)基礎(chǔ)類構(gòu)建器的調(diào)用。這意味著在它之前不能出現(xiàn)任何東西。正如大家在第9章會(huì)看到的那樣,這同時(shí)也會(huì)防止衍生類構(gòu)建器捕獲來自一個(gè)基礎(chǔ)類的任何違例事件。顯然,這有時(shí)會(huì)為我們?cè)斐刹槐恪?.3 合成與繼承的結(jié)合許多時(shí)候都要求將合成與繼承兩種技術(shù)結(jié)合起來使用。下面這個(gè)例子展示了如何同時(shí)采用繼承與合成技術(shù),從而創(chuàng)建一個(gè)更復(fù)雜的類,同時(shí)進(jìn)行必要的構(gòu)建器初始化工作:226-228頁程序盡管編譯器會(huì)強(qiáng)迫我們對(duì)基礎(chǔ)類進(jìn)行初始化,并要求我們?cè)跇?gòu)建器最開頭做這一工作,但它并不會(huì)監(jiān)視我們是否正確初始化了成員對(duì)象。所以對(duì)此必須特別加以留意。6.3.1 確保正確的清除Java不具備

19、象C+的“破壞器”那樣的概念。在C+中,一旦破壞(清除)一個(gè)對(duì)象,就會(huì)自動(dòng)調(diào)用破壞器方法。之所以將其省略,大概是由于在Java中只需簡(jiǎn)單地忘記對(duì)象,不需強(qiáng)行破壞它們。垃圾收集器會(huì)在必要的時(shí)候自動(dòng)回收內(nèi)存。垃圾收集器大多數(shù)時(shí)候都能很好地工作,但在某些情況下,我們的類可能在自己的存在時(shí)期采取一些行動(dòng),而這些行動(dòng)要求必須進(jìn)行明確的清除工作。正如第4章已經(jīng)指出的那樣,我們并不知道垃圾收集器什么時(shí)候才會(huì)顯身,或者說不知它何時(shí)會(huì)調(diào)用。所以一旦希望為一個(gè)類清除什么東西,必須寫一個(gè)特別的方法,明確、專門地來做這件事情。同時(shí),還要讓客戶程序員知道他們必須調(diào)用這個(gè)方法。而在所有這一切的后面,就如第9章(違例控制)

20、要詳細(xì)解釋的那樣,必須將這樣的清除代碼置于一個(gè)finally從句中,從而防范任何可能出現(xiàn)的違例事件。下面介紹的是一個(gè)計(jì)算機(jī)輔助設(shè)計(jì)系統(tǒng)的例子,它能在屏幕上描繪圖形:229-230頁程序這個(gè)系統(tǒng)中的所有東西都屬于某種Shape(幾何形狀)。Shape本身是一種Object(對(duì)象),因?yàn)樗菑母惷鞔_繼承的。每個(gè)類都重新定義了Shape的cleanup()方法,同時(shí)還要用super調(diào)用那個(gè)方法的基礎(chǔ)類版本。盡管對(duì)象存在期間調(diào)用的所有方法都可負(fù)責(zé)做一些要求清除的工作,但對(duì)于特定的Shape類Circle(圓)、Triangle(三角形)以及Line(直線),它們都擁有自己的構(gòu)建器,能完成“作圖”(d

21、raw)任務(wù)。每個(gè)類都有它們自己的cleanup()方法,用于將非內(nèi)存的東西恢復(fù)回對(duì)象存在之前的景象。在main()中,可看到兩個(gè)新關(guān)鍵字:try和finally。我們要到第9章才會(huì)向大家正式引薦它們。其中,try關(guān)鍵字指出后面跟隨的塊(由花括號(hào)定界)是一個(gè)“警戒區(qū)”。也就是說,它會(huì)受到特別的待遇。其中一種待遇就是:該警戒區(qū)后面跟隨的finally從句的代碼肯定會(huì)得以執(zhí)行不管try塊到底存不存在(通過違例控制技術(shù),try塊可有多種不尋常的應(yīng)用)。在這里,finally從句的意思是“總是為x調(diào)用cleanup(),無論會(huì)發(fā)生什么事情”。這些關(guān)鍵字將在第9章進(jìn)行全面、完整的解釋。在自己的清除方法中

22、,必須注意對(duì)基礎(chǔ)類以及成員對(duì)象清除方法的調(diào)用順序假若一個(gè)子對(duì)象要以另一個(gè)為基礎(chǔ)。通常,應(yīng)采取與C+編譯器對(duì)它的“破壞器”采取的同樣的形式:首先完成與類有關(guān)的所有特殊工作(可能要求基礎(chǔ)類元素仍然可見),然后調(diào)用基礎(chǔ)類清除方法,就象這兒演示的那樣。許多情況下,清除可能并不是個(gè)問題;只需讓垃圾收集器盡它的職責(zé)即可。但一旦必須由自己明確清除,就必須特別謹(jǐn)慎,并要求周全的考慮。1. 垃圾收集的順序不能指望自己能確切知道何時(shí)會(huì)開始垃圾收集。垃圾收集器可能永遠(yuǎn)不會(huì)得到調(diào)用。即使得到調(diào)用,它也可能以自己愿意的任何順序回收對(duì)象。除此以外,Java 1.0實(shí)現(xiàn)的垃圾收集器機(jī)制通常不會(huì)調(diào)用finalize()方法。

23、除內(nèi)存的回收以外,其他任何東西都最好不要依賴?yán)占鬟M(jìn)行回收。若想明確地清除什么,請(qǐng)制作自己的清除方法,而且不要依賴finalize()。然而正如以前指出的那樣,可強(qiáng)迫Java1.1調(diào)用所有收尾模塊(Finalizer)。6.3.2 名字的隱藏只有C+程序員可能才會(huì)驚訝于名字的隱藏,因?yàn)樗墓ぷ髟砼c在C+里是完全不同的。如果Java基礎(chǔ)類有一個(gè)方法名被“過載”使用多次,在衍生類里對(duì)那個(gè)方法名的重新定義就不會(huì)隱藏任何基礎(chǔ)類的版本。所以無論方法在這一級(jí)還是在一個(gè)基礎(chǔ)類中定義,過載都會(huì)生效:232頁程序正如下一章會(huì)講到的那樣,很少會(huì)用與基礎(chǔ)類里完全一致的簽名和返回類型來覆蓋同名的方法,否則會(huì)使人

24、感到迷惑(這正是C+不允許那樣做的原因,所以能夠防止產(chǎn)生一些不必要的錯(cuò)誤)。6.4 到底選擇合成還是繼承無論合成還是繼承,都允許我們將子對(duì)象置于自己的新類中。大家或許會(huì)奇怪兩者間的差異,以及到底該如何選擇。如果想利用新類內(nèi)部一個(gè)現(xiàn)有類的特性,而不想使用它的接口,通常應(yīng)選擇合成。也就是說,我們可嵌入一個(gè)對(duì)象,使自己能用它實(shí)現(xiàn)新類的特性。但新類的用戶會(huì)看到我們已定義的接口,而不是來自嵌入對(duì)象的接口。考慮到這種效果,我們需在新類里嵌入現(xiàn)有類的private對(duì)象。有些時(shí)候,我們想讓類用戶直接訪問新類的合成。也就是說,需要將成員對(duì)象的屬性變?yōu)閜ublic。成員對(duì)象會(huì)將自身隱藏起來,所以這是一種安全的做法

25、。而且在用戶知道我們準(zhǔn)備合成一系列組件時(shí),接口就更容易理解。car(汽車)對(duì)象便是一個(gè)很好的例子:233-234頁程序由于汽車的裝配是故障分析時(shí)需要考慮的一項(xiàng)因素(并非只是基礎(chǔ)設(shè)計(jì)簡(jiǎn)單的一部分),所以有助于客戶程序員理解如何使用類,而且類創(chuàng)建者的編程復(fù)雜程度也會(huì)大幅度降低。如選擇繼承,就需要取得一個(gè)現(xiàn)成的類,并制作它的一個(gè)特殊版本。通常,這意味著我們準(zhǔn)備使用一個(gè)常規(guī)用途的類,并根據(jù)特定的需求對(duì)其進(jìn)行定制。只需稍加想象,就知道自己不能用一個(gè)車輛對(duì)象來合成一輛汽車汽車并不“包含”車輛;相反,它“屬于”車輛的一種類別。“屬于”關(guān)系是用繼承來表達(dá)的,而“包含”關(guān)系是用合成來表達(dá)的。6.5 protec

26、ted現(xiàn)在我們已理解了繼承的概念,protected這個(gè)關(guān)鍵字最后終于有了意義。在理想情況下,private成員隨時(shí)都是“私有”的,任何人不得訪問。但在實(shí)際應(yīng)用中,經(jīng)常想把某些東西深深地藏起來,但同時(shí)允許訪問衍生類的成員。protected關(guān)鍵字可幫助我們做到這一點(diǎn)。它的意思是“它本身是私有的,但可由從這個(gè)類繼承的任何東西或者同一個(gè)包內(nèi)的其他任何東西訪問”。也就是說,Java中的protected會(huì)成為進(jìn)入“友好”狀態(tài)。我們采取的最好的做法是保持成員的private狀態(tài)無論如何都應(yīng)保留對(duì)基 礎(chǔ)的實(shí)施細(xì)節(jié)進(jìn)行修改的權(quán)利。在這一前提下,可通過protected方法允許類的繼承者進(jìn)行受到控制的訪問:

27、235頁程序可以看到,change()擁有對(duì)set()的訪問權(quán)限,因?yàn)樗膶傩允莗rotected(受到保護(hù)的)。6.6 累積開發(fā)繼承的一個(gè)好處是它支持“累積開發(fā)”,允許我們引入新的代碼,同時(shí)不會(huì)為現(xiàn)有代碼造成錯(cuò)誤。這樣可將新錯(cuò)誤隔離到新代碼里。通過從一個(gè)現(xiàn)成的、功能性的類繼承,同時(shí)增添成員新的數(shù)據(jù)成員及方法(并重新定義現(xiàn)有方法),我們可保持現(xiàn)有代碼原封不動(dòng)(另外有人也許仍在使用它),不會(huì)為其引入自己的編程錯(cuò)誤。一旦出現(xiàn)錯(cuò)誤,就知道它肯定是由于自己的新代碼造成的。這樣一來,與修改現(xiàn)有代碼的主體相比,改正錯(cuò)誤所需的時(shí)間和精力就可以少很多。類的隔離效果非常好,這是許多程序員事先沒有預(yù)料到的。甚至不

28、需要方法的源代碼來實(shí)現(xiàn)代碼的再生。最多只需要導(dǎo)入一個(gè)包(這對(duì)于繼承和合并都是成立的)。大家要記住這樣一個(gè)重點(diǎn):程序開發(fā)是一個(gè)不斷遞增或者累積的過程,就象人們學(xué)習(xí)知識(shí)一樣。當(dāng)然可根據(jù)要求進(jìn)行盡可能多的分析,但在一個(gè)項(xiàng)目的設(shè)計(jì)之初,誰都不可能提前獲知所有的答案。如果能將自己的項(xiàng)目看作一個(gè)有機(jī)的、能不斷進(jìn)步的生物,從而不斷地發(fā)展和改進(jìn)它,就有望獲得更大的成功以及更直接的反饋。盡管繼承是一種非常有用的技術(shù),但在某些情況下,特別是在項(xiàng)目穩(wěn)定下來以后,仍然需要從新的角度考察自己的類結(jié)構(gòu),將其收縮成一個(gè)更靈活的結(jié)構(gòu)。請(qǐng)記住,繼承是對(duì)一種特殊關(guān)系的表達(dá),意味著“這個(gè)新類屬于那個(gè)舊類的一種類型”。我們的程序不應(yīng)

29、糾纏于一些細(xì)樹末節(jié),而應(yīng)著眼于創(chuàng)建和操作各種類型的對(duì)象,用它們表達(dá)出來自“問題空間”的一個(gè)模型。6.7 上溯造型繼承最值得注意的地方就是它沒有為新類提供方法。繼承是對(duì)新類和基礎(chǔ)類之間的關(guān)系的一種表達(dá)。可這樣總結(jié)該關(guān)系:“新類屬于現(xiàn)有類的一種類型”。這種表達(dá)并不僅僅是對(duì)繼承的一種形象化解釋,繼承是直接由語言提供支持的。作為一個(gè)例子,大家可考慮一個(gè)名為Instrument的基礎(chǔ)類,它用于表示樂器;另一個(gè)衍生類叫作Wind。由于繼承意味著基礎(chǔ)類的所有方法亦可在衍生出來的類中使用,所以我們發(fā)給基礎(chǔ)類的任何消息亦可發(fā)給衍生類。若Instrument類有一個(gè)play()方法,則Wind設(shè)備也會(huì)有這個(gè)方法。

30、這意味著我們能肯定地認(rèn)為一個(gè)Wind對(duì)象也是Instrument的一種類型。下面這個(gè)例子揭示出編譯器如何提供對(duì)這一概念的支持:236-237頁程序這個(gè)例子中最有趣的無疑是tune()方法,它能接受一個(gè)Instrument句柄。但在Wind.main()中,tune()方法是通過為其賦予一個(gè)Wind句柄來調(diào)用的。由于Java對(duì)類型檢查特別嚴(yán)格,所以大家可能會(huì)感到很奇怪,為什么接收一種類型的方法也能接收另一種類型呢?但是,我們一定要認(rèn)識(shí)到一個(gè)Wind對(duì)象也是一個(gè)Instrument對(duì)象。而且對(duì)于不在Wind中的一個(gè)Instrument(樂器),沒有方法可以由tune()調(diào)用。在tune()中,代碼

31、適用于Instrument以及從Instrument衍生出來的任何東西。在這里,我們將從一個(gè)Wind句柄轉(zhuǎn)換成一個(gè)Instrument句柄的行為叫作“上溯造型”。6.7.1 何謂“上溯造型”?之所以叫作這個(gè)名字,除了有一定的歷史原因外,也是由于在傳統(tǒng)意義上,類繼承圖的畫法是根位于最頂部,再逐漸向下擴(kuò)展(當(dāng)然,可根據(jù)自己的習(xí)慣用任何方法描繪這種圖)。因素,Wind.java的繼承圖就象下面這個(gè)樣子:237頁圖由于造型的方向是從衍生類到基礎(chǔ)類,箭頭朝上,所以通常把它叫作“上溯造型”,即Upcasting。上溯造型肯定是安全的,因?yàn)槲覀兪菑囊粋€(gè)更特殊的類型到一個(gè)更常規(guī)的類型。換言之,衍生類是基礎(chǔ)類的

32、一個(gè)超集。它可以包含比基礎(chǔ)類更多的方法,但它至少包含了基礎(chǔ)類的方法。進(jìn)行上溯造型的時(shí)候,類接口可能出現(xiàn)的唯一一個(gè)問題是它可能丟失方法,而不是贏得這些方法。這便是在沒有任何明確的造型或者其他特殊標(biāo)注的情況下,編譯器為什么允許上溯造型的原因所在。也可以執(zhí)行下溯造型,但這時(shí)會(huì)面臨第11章要詳細(xì)講述的一種困境。1. 再論合成與繼承在面向?qū)ο蟮某绦蛟O(shè)計(jì)中,創(chuàng)建和使用代碼最可能采取的一種做法是:將數(shù)據(jù)和方法統(tǒng)一封裝到一個(gè)類里,并且使用那個(gè)類的對(duì)象。有些時(shí)候,需通過“合成”技術(shù)用現(xiàn)成的類來構(gòu)造新類。而繼承是最少見的一種做法。因此,盡管繼承在學(xué)習(xí)OOP的過程中得到了大量的強(qiáng)調(diào),但并不意味著應(yīng)該盡可能地到處使用

33、它。相反,使用它時(shí)要特別慎重。只有在清楚知道繼承在所有方法中最有效的前提下,才可考慮它。為判斷自己到底應(yīng)該選用合成還是繼承,一個(gè)最簡(jiǎn)單的辦法就是考慮是否需要從新類上溯造型回基礎(chǔ)類。若必須上溯,就需要繼承。但如果不需要上溯造型,就應(yīng)提醒自己防止繼承的濫用。在下一章里(多形性),會(huì)向大家介紹必須進(jìn)行上溯造型的一種場(chǎng)合。但只要記住經(jīng)常問自己“我真的需要上溯造型嗎”,對(duì)于合成還是繼承的選擇就不應(yīng)該是個(gè)太大的問題。6.8 final關(guān)鍵字由于語境(應(yīng)用環(huán)境)不同,final關(guān)鍵字的含義可能會(huì)稍微產(chǎn)生一些差異。但它最一般的意思就是聲明“這個(gè)東西不能改變”。之所以要禁止改變,可能是考慮到兩方面的因素:設(shè)計(jì)或

34、效率。由于這兩個(gè)原因頗有些區(qū)別,所以也許會(huì)造成final關(guān)鍵字的誤用。在接下去的小節(jié)里,我們將討論final關(guān)鍵字的三種應(yīng)用場(chǎng)合:數(shù)據(jù)、方法以及類。6.8.1 final數(shù)據(jù)許多程序設(shè)計(jì)語言都有自己的辦法告訴編譯器某個(gè)數(shù)據(jù)是“常數(shù)”。常數(shù)主要應(yīng)用于下述兩個(gè)方面:(1) 編譯期常數(shù),它永遠(yuǎn)不會(huì)改變(2) 在運(yùn)行期初始化的一個(gè)值,我們不希望它發(fā)生變化對(duì)于編譯期的常數(shù),編譯器(程序)可將常數(shù)值“封裝”到需要的計(jì)算過程里。也就是說,計(jì)算可在編譯期間提前執(zhí)行,從而節(jié)省運(yùn)行時(shí)的一些開銷。在Java中,這些形式的常數(shù)必須屬于基本數(shù)據(jù)類型(Primitives),而且要用final關(guān)鍵字進(jìn)行表達(dá)。在對(duì)這樣的一

35、個(gè)常數(shù)進(jìn)行定義的時(shí)候,必須給出一個(gè)值。無論static還是final字段,都只能存儲(chǔ)一個(gè)數(shù)據(jù),而且不得改變。若隨同對(duì)象句柄使用final,而不是基本數(shù)據(jù)類型,它的含義就稍微讓人有點(diǎn)兒迷糊了。對(duì)于基本數(shù)據(jù)類型,final會(huì)將值變成一個(gè)常數(shù);但對(duì)于對(duì)象句柄,final會(huì)將句柄變成一個(gè)常數(shù)。進(jìn)行聲明時(shí),必須將句柄初始化到一個(gè)具體的對(duì)象。而且永遠(yuǎn)不能將句柄變成指向另一個(gè)對(duì)象。然而,對(duì)象本身是可以修改的。Java對(duì)此未提供任何手段,可將一個(gè)對(duì)象直接變成一個(gè)常數(shù)(但是,我們可自己編寫一個(gè)類,使其中的對(duì)象具有“常數(shù)”效果)。這一限制也適用于數(shù)組,它也屬于對(duì)象。下面是演示final字段用法的一個(gè)例子:29-2

36、40頁程序由于i1和I2都是具有final屬性的基本數(shù)據(jù)類型,并含有編譯期的值,所以它們除了能作為編譯期的常數(shù)使用外,在任何導(dǎo)入方式中也不會(huì)出現(xiàn)任何不同。I3是我們體驗(yàn)此類常數(shù)定義時(shí)更典型的一種方式:public表示它們可在包外使用;Static強(qiáng)調(diào)它們只有一個(gè);而final表明它是一個(gè)常數(shù)。注意對(duì)于含有固定初始化值(即編譯期常數(shù))的fianl static基本數(shù)據(jù)類型,它們的名字根據(jù)規(guī)則要全部采用大寫。也要注意i5在編譯期間是未知的,所以它沒有大寫。不能由于某樣?xùn)|西的屬性是final,就認(rèn)定它的值能在編譯時(shí)期知道。i4和i5向大家證明了這一點(diǎn)。它們?cè)谶\(yùn)行期間使用隨機(jī)生成的數(shù)字。例子的這一部分

37、也向大家揭示出將final值設(shè)為static和非static之間的差異。只有當(dāng)值在運(yùn)行期間初始化的前提下,這種差異才會(huì)揭示出來。因?yàn)榫幾g期間的值被編譯器認(rèn)為是相同的。這種差異可從輸出結(jié)果中看出:240-241頁程序注意對(duì)于fd1和fd2來說,i4的值是唯一的,但i5的值不會(huì)由于創(chuàng)建了另一個(gè)FinalData對(duì)象而發(fā)生改變。那是因?yàn)樗膶傩允莝tatic,而且在載入時(shí)初始化,而非每創(chuàng)建一個(gè)對(duì)象時(shí)初始化。從v1到v4的變量向我們揭示出final句柄的含義。正如大家在main()中看到的那樣,并不能認(rèn)為由于v2屬于final,所以就不能再改變它的值。然而,我們確實(shí)不能再將v2綁定到一個(gè)新對(duì)象,因?yàn)樗?/p>

38、的屬性是final。這便是final對(duì)于一個(gè)句柄的確切含義。我們會(huì)發(fā)現(xiàn)同樣的含義亦適用于數(shù)組,后者只不過是另一種類型的句柄而已。將句柄變成final看起來似乎不如將基本數(shù)據(jù)類型變成final那么有用。2. 空白finalJava 1.1允許我們創(chuàng)建“空白final”,它們屬于一些特殊的字段。盡管被聲明成final,但卻未得到一個(gè)初始值。無論在哪種情況下,空白final都必須在實(shí)際使用前得到正確的初始化。而且編譯器會(huì)主動(dòng)保證這一規(guī)定得以貫徹。然而,對(duì)于final關(guān)鍵字的各種應(yīng)用,空白final具有最大的靈活性。舉個(gè)例子來說,位于類內(nèi)部的一個(gè)final字段現(xiàn)在對(duì)每個(gè)對(duì)象都可以有所不同,同時(shí)依然保持

39、其“不變”的本質(zhì)。下面列出一個(gè)例子:241-242頁程序現(xiàn)在強(qiáng)行要求我們對(duì)final進(jìn)行賦值處理要么在定義字段時(shí)使用一個(gè)表達(dá) 式,要么在每個(gè)構(gòu)建器中。這樣就可以確保final字段在使用前獲得正確的初始化。3. final自變量Java 1.1允許我們將自變量設(shè)成final屬性,方法是在自變量列表中對(duì)它們進(jìn)行適當(dāng)?shù)穆暶鳌_@意味著在一個(gè)方法的內(nèi)部,我們不能改變自變量句柄指向的東西。如下所示:242頁程序注意此時(shí)仍然能為final自變量分配一個(gè)null(空)句柄,同時(shí)編譯器不會(huì)捕獲它。這與我們對(duì)非final自變量采取的操作是一樣的。方法f()和g()向我們展示出基本類型的自變量為final時(shí)會(huì)發(fā)生什

40、么情況:我們只能讀取自變量,不可改變它。6.8.2 final方法之所以要使用final方法,可能是出于對(duì)兩方面理由的考慮。第一個(gè)是為方法“上鎖”,防止任何繼承類改變它的本來含義。設(shè)計(jì)程序時(shí),若希望一個(gè)方法的行為在繼承期間保持不變,而且不可被覆蓋或改寫,就可以采取這種做法。采用final方法的第二個(gè)理由是程序執(zhí)行的效率。將一個(gè)方法設(shè)成final后,編譯器就可以把對(duì)那個(gè)方法的所有調(diào)用都置入“嵌入”調(diào)用里。只要編譯器發(fā)現(xiàn)一個(gè)final方法調(diào)用,就會(huì)(根據(jù)它自己的判斷)忽略為執(zhí)行方法調(diào)用機(jī)制而采取的常規(guī)代碼插入方法(將自變量壓入堆棧;跳至方法代碼并執(zhí)行它;跳回來;清除堆棧自變量;最后對(duì)返回值進(jìn)行處理

41、)。相反,它會(huì)用方法主體內(nèi)實(shí)際代碼的一個(gè)副本來替換方法調(diào)用。這樣做可避免方法調(diào)用時(shí)的系統(tǒng)開銷。當(dāng)然,若方法體積太大,那么程序也會(huì)變得雍腫,可能受到到不到嵌入代碼所帶來的任何性能提升。因?yàn)槿魏翁嵘急换ㄔ诜椒▋?nèi)部的時(shí)間抵消了。Java編譯器能自動(dòng)偵測(cè)這些情況,并頗為“明智”地決定是否嵌入一個(gè)final方法。然而,最好還是不要完全相信編譯器能正確地作出所有判斷。通常,只有在方法的代碼量非常少,或者想明確禁止方法被覆蓋的時(shí)候,才應(yīng)考慮將一個(gè)方法設(shè)為final。類內(nèi)所有private方法都自動(dòng)成為final。由于我們不能訪問一個(gè)private方法,所以它絕對(duì)不會(huì)被其他方法覆蓋(若強(qiáng)行這樣做,編譯器會(huì)給

42、出錯(cuò)誤提示)。可為一個(gè)private方法添加final指示符,但卻不能為那個(gè)方法提供任何額外的含義。6.8.3 final類如果說整個(gè)類都是final(在它的定義前冠以final關(guān)鍵字),就表明自己不希望從這個(gè)類繼承,或者不允許其他任何人采取這種操作。換言之,出于這樣或那樣的原因,我們的類肯定不需要進(jìn)行任何改變;或者出于安全方面的理由,我們不希望進(jìn)行子類化(子類處理)。除此以外,我們或許還考慮到執(zhí)行效率的問題,并想確保涉及這個(gè)類各對(duì)象的所有行動(dòng)都要盡可能地有效。如下所示:244頁程序注意數(shù)據(jù)成員既可以是final,也可以不是,取決于我們具體選擇。應(yīng)用于final的規(guī)則同樣適用于數(shù)據(jù)成員,無論類

43、是否被定義成final。將類定義成final后,結(jié)果只是禁止進(jìn)行繼承沒有更多的限制。然而,由于它禁止了繼承,所以一個(gè)final類中的所有方法都默認(rèn)為final。因?yàn)榇藭r(shí)再也無法覆蓋它們。所以與我們將一個(gè)方法明確聲明為final一樣,編譯器此時(shí)有相同的效率選擇。可為final類內(nèi)的一個(gè)方法添加final指示符,但這樣做沒有任何意義。6.8.4 final的注意事項(xiàng)設(shè)計(jì)一個(gè)類時(shí),往往需要考慮是否將一個(gè)方法設(shè)為final。可能會(huì)覺得使用自己的類時(shí)執(zhí)行效率非常重要,沒有人想覆蓋自己的方法。這種想法在某些時(shí)候是正確的。但要慎重作出自己的假定。通常,我們很難預(yù)測(cè)一個(gè)類以后會(huì)以什么樣的形式再生或重復(fù)利用。常

44、規(guī)用途的類尤其如此。若將一個(gè)方法定義成final,就可能杜絕了在其他程序員的項(xiàng)目中對(duì)自己的類進(jìn)行繼承的途徑,因?yàn)槲覀兏緵]有想到它會(huì)象那樣使用。標(biāo)準(zhǔn)Java庫是闡述這一觀點(diǎn)的最好例子。其中特別常用的一個(gè)類是Vector。如果我們考慮代碼的執(zhí)行效率,就會(huì)發(fā)現(xiàn)只有不把任何方法設(shè)為final,才能使其發(fā)揮更大的作用。我們很容易就會(huì)想到自己應(yīng)繼承和覆蓋如此有用的一個(gè)類,但它的設(shè)計(jì)者卻否定了我們的想法。但我們至少可以用兩個(gè)理由來反駁他們。首先,Stack(堆棧)是從Vector繼承來的,亦即Stack“是”一個(gè)Vector,這種說法是不確切的。其次,對(duì)于Vector許多重要的方法,如addElement

45、()以及elementAt()等,它們都變成了synchronized(同步的)。正如在第14章要講到的那樣,這會(huì)造成顯著的性能開銷,可能會(huì)把final提供的性能改善抵銷得一干二凈。因此,程序員不得不猜測(cè)到底應(yīng)該在哪里進(jìn)行優(yōu)化。在標(biāo)準(zhǔn)庫里居然采用了如此笨拙的設(shè)計(jì),真不敢想象會(huì)在程序員里引發(fā)什么樣的情緒。另一個(gè)值得注意的是Hashtable(散列表),它是另一個(gè)重要的標(biāo)準(zhǔn)類。該類沒有采用任何final方法。正如我們?cè)诒緯渌胤教岬降哪菢樱@然一些類的設(shè)計(jì)人員與其他設(shè)計(jì)人員有著全然不同的素質(zhì)(注意比較Hashtable極短的方法名與Vecor的方法名)。對(duì)類庫的用戶來說,這顯然是不應(yīng)該如此輕易就

46、能看出的。一個(gè)產(chǎn)品的設(shè)計(jì)變得不一致后,會(huì)加大用戶的工作量。這也從另一個(gè)側(cè)面強(qiáng)調(diào)了代碼設(shè)計(jì)與檢查時(shí)需要很強(qiáng)的責(zé)任心。6.9 初始化和類裝載在許多傳統(tǒng)語言里,程序都是作為啟動(dòng)過程的一部分一次性載入的。隨后進(jìn)行的是初始化,再是正式執(zhí)行程序。在這些語言中,必須對(duì)初始化過程進(jìn)行慎重的控制,保證static數(shù)據(jù)的初始化不會(huì)帶來麻煩。比如在一個(gè)static數(shù)據(jù)獲得初始化之前,就有另一個(gè)static數(shù)據(jù)希望它是一個(gè)有效值,那么在C+中就會(huì)造成問題。Java則沒有這樣的問題,因?yàn)樗捎昧瞬煌难b載方法。由于Java中的一切東西都是對(duì)象,所以許多活動(dòng)變得更加簡(jiǎn)單,這個(gè)問題便是其中的一例。正如下一章會(huì)講到的那樣,每個(gè)對(duì)象的代碼都存在于獨(dú)立的文件中。除非真的需要代碼,

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。

評(píng)論

0/150

提交評(píng)論