從技術(shù)角度說,OOP(面向?qū)ο蟪绦蛟O(shè)計)只是涉及抽象的數(shù)據(jù)類型、繼承以及多形性杠愧,但另一些問題也可能顯得非常重要利朵。本節(jié)將就這些問題進(jìn)行探討律想。
最重要的問題之一是對象的創(chuàng)建及破壞方式。對象需要的數(shù)據(jù)位于哪兒绍弟,如何控制對象的“存在時間”呢技即?針對這個問題,解決的方案是各異其趣的樟遣。C++認(rèn)為程序的執(zhí)行效率是最重要的一個問題而叼,所以它允許程序員作出選擇。為獲得最快的運行速度豹悬,存儲以及存在時間可在編寫程序時決定葵陵,只需將對象放置在堆棧(有時也叫作自動或定域變量)或者靜態(tài)存儲區(qū)域即可。這樣便為存儲空間的分配和釋放提供了一個優(yōu)先級瞻佛。某些情況下脱篙,這種優(yōu)先級的控制是非常有價值的。然而涤久,我們同時也犧牲了靈活性涡尘,因為在編寫程序時,必須知道對象的準(zhǔn)確的數(shù)量响迂、存在時間考抄、以及類型。如果要解決的是一個較常規(guī)的問題蔗彤,如計算機(jī)輔助設(shè)計川梅、倉儲管理或者空中交通控制,這一方法就顯得太局限了然遏。
第二個方法是在一個內(nèi)存池中動態(tài)創(chuàng)建對象贫途,該內(nèi)存池亦叫“堆”或者“內(nèi)存堆”。若采用這種方式待侵,除非進(jìn)入運行期丢早,否則根本不知道到底需要多少個對象,也不知道它們的存在時間有多長,以及準(zhǔn)確的類型是什么怨酝。這些參數(shù)都在程序正式運行時才決定的傀缩。若需一個新對象,只需在需要它的時候在內(nèi)存堆里簡單地創(chuàng)建它即可农猬。由于存儲空間的管理是運行期間動態(tài)進(jìn)行的赡艰,所以在內(nèi)存堆里分配存儲空間的時間比在堆棧里創(chuàng)建的時間長得多(在堆棧里創(chuàng)建存儲空間一般只需要一個簡單的指令,將堆棧指針向下或向下移動即可)斤葱。由于動態(tài)創(chuàng)建方法使對象本來就傾向于復(fù)雜慷垮,所以查找存儲空間以及釋放它所需的額外開銷不會為對象的創(chuàng)建造成明顯的影響。除此以外揍堕,更大的靈活性對于常規(guī)編程問題的解決是至關(guān)重要的料身。
C++允許我們決定是在寫程序時創(chuàng)建對象,還是在運行期間創(chuàng)建鹤啡,這種控制方法更加靈活惯驼。大家或許認(rèn)為既然它如此靈活,那么無論如何都應(yīng)在內(nèi)存堆里創(chuàng)建對象递瑰,而不是在堆棧中創(chuàng)建祟牲。但還要考慮另外一個問題,亦即對象的“存在時間”或者“生存時間”(Lifetime)抖部。若在堆椝当矗或者靜態(tài)存儲空間里創(chuàng)建一個對象,編譯器會判斷對象的持續(xù)時間有多長慎颗,到時會自動“破壞”或者“清除”它乡恕。程序員可用兩種方法來破壞一個對象:用程序化的方式?jīng)Q定何時破壞對象,或者利用由運行環(huán)境提供的一種“垃圾收集器”特性俯萎,自動尋找那些不再使用的對象傲宜,并將其清除。當(dāng)然夫啊,垃圾收集器顯得方便得多函卒,但要求所有應(yīng)用程序都必須容忍垃圾收集器的存在,并能默許隨垃圾收集帶來的額外開銷撇眯。但這并不符合C++語言的設(shè)計宗旨报嵌,所以未能包括到C++里。但Java確實提供了一個垃圾收集器(Smalltalk也有這樣的設(shè)計熊榛;盡管Delphi默認(rèn)為沒有垃圾收集器锚国,但可選擇安裝;而C++亦可使用一些由其他公司開發(fā)的垃圾收集產(chǎn)品)玄坦。
本節(jié)剩下的部分將討論操縱對象時要考慮的另一些因素血筑。
1.7.1 集合與繼承器
針對一個特定問題的解決,如果事先不知道需要多少個對象,或者它們的持續(xù)時間有多長豺总,那么也不知道如何保存那些對象梆砸。既然如此,怎樣才能知道那些對象要求多少空間呢园欣?事先上根本無法提前知道,除非進(jìn)入運行期休蟹。
在面向?qū)ο蟮脑O(shè)計中沸枯,大多數(shù)問題的解決辦法似乎都有些輕率——只是簡單地創(chuàng)建另一種類型的對象。用于解決特定問題的新型對象容納了指向其他對象的句柄赂弓。當(dāng)然绑榴,也可以用數(shù)組來做同樣的事情,那是大多數(shù)語言都具有的一種功能盈魁。但不能只看到這一點翔怎。這種新對象通常叫作“集合”(亦叫作一個“容器”,但AWT在不同的場合應(yīng)用了這個術(shù)語杨耙,所以本書將一直沿用“集合”的稱呼赤套。在需要的時候,集合會自動擴(kuò)充自己珊膜,以便適應(yīng)我們在其中置入的任何東西容握。所以我們事先不必知道要在一個集合里容下多少東西。只需創(chuàng)建一個集合车柠,以后的工作讓它自己負(fù)責(zé)好了剔氏。
幸運的是,設(shè)計優(yōu)良的OOP語言都配套提供了一系列集合竹祷。在C++中谈跛,它們是以“標(biāo)準(zhǔn)模板庫”(STL)的形式提供的。Object Pascal用自己的“可視組件庫”(VCL)提供集合塑陵。Smalltalk提供了一套非常完整的集合感憾。而Java也用自己的標(biāo)準(zhǔn)庫提供了集合。在某些庫中猿妈,一個常規(guī)集合便可滿足人們的大多數(shù)要求吹菱;而在另一些庫中(特別是C++的庫),則面向不同的需求提供了不同類型的集合彭则。例如鳍刷,可以用一個矢量統(tǒng)一對所有元素的訪問方式;一個鏈接列表則用于保證所有元素的插入統(tǒng)一俯抖。所以我們能根據(jù)自己的需要選擇適當(dāng)?shù)念愋褪涔稀F渲邪㈥犃小⑸⒘斜碛却А渖Π ⒍褩5鹊取?/p>
所有集合都提供了相應(yīng)的讀寫功能。將某樣?xùn)|西置入集合時北戏,采用的方式是十分明顯的负芋。有一個叫作“推”(Push)、“添加”(Add)或其他類似名字的函數(shù)用于做這件事情嗜愈。但將數(shù)據(jù)從集合中取出的時候旧蛾,方式卻并不總是那么明顯。如果是一個數(shù)組形式的實體蠕嫁,比如一個矢量(Vector)锨天,那么也許能用索引運算符或函數(shù)。但在許多情況下剃毒,這樣做往往會無功而返病袄。此外,單選定函數(shù)的功能是非常有限的赘阀。如果想對集合中的一系列元素進(jìn)行操縱或比較益缠,而不是僅僅面向一個,這時又該怎么辦呢纤壁?
辦法就是使用一個“繼續(xù)器”(Iterator)左刽,它屬于一種對象狂打,負(fù)責(zé)選擇集合內(nèi)的元素瘸味,并把它們提供給繼承器的用戶嗤无。作為一個類寨蹋,它也提供了一級抽象迟隅。利用這一級抽象贪庙,可將集合細(xì)節(jié)與用于訪問那個集合的代碼隔離開牡昆。通過繼承器的作用砰诵,集合被抽象成一個簡單的序列雨席。繼承器允許我們遍歷那個序列菩咨,同時毋需關(guān)心基礎(chǔ)結(jié)構(gòu)是什么——換言之,不管它是一個矢量陡厘、一個鏈接列表抽米、一個堆棧,還是其他什么東西糙置。這樣一來云茸,我們就可以靈活地改變基礎(chǔ)數(shù)據(jù),不會對程序里的代碼造成干擾谤饭。Java最開始(在1.0和1.1版中)提供的是一個標(biāo)準(zhǔn)繼承器标捺,名為Enumeration(枚舉)懊纳,為它的所有集合類提供服務(wù)。Java 1.2新增一個更復(fù)雜的集合庫亡容,其中包含了一個名為Iterator的繼承器嗤疯,可以做比老式的Enumeration更多的事情。
從設(shè)計角度出發(fā)闺兢,我們需要的是一個全功能的序列茂缚。通過對它的操縱,應(yīng)該能解決自己的問題屋谭。如果一種類型的序列即可滿足我們的所有要求阱佛,那么完全沒有必要再換用不同的類型。有兩方面的原因促使我們需要對集合作出選擇戴而。首先,集合提供了不同的接口類型以及外部行為翩蘸。堆棧的接口與行為與隊列的不同所意,而隊列的接口與行為又與一個集(Set)或列表的不同。利用這個特征催首,我們解決問題時便有更大的靈活性扶踊。
其次,不同的集合在進(jìn)行特定操作時往往有不同的效率郎任。最好的例子便是矢量(Vector)和列表(List)的區(qū)別秧耗。它們都屬于簡單的序列,擁有完全一致的接口和外部行為舶治。但在執(zhí)行一些特定的任務(wù)時分井,需要的開銷卻是完全不同的。對矢量內(nèi)的元素進(jìn)行的隨機(jī)訪問(存让姑汀)是一種常時操作尺锚;無論我們選擇的選擇是什么,需要的時間量都是相同的惜浅。但在一個鏈接列表中瘫辩,若想到處移動,并隨機(jī)挑選一個元素坛悉,就需付出“慘重”的代價伐厌。而且假設(shè)某個元素位于列表較遠(yuǎn)的地方,找到它所需的時間也會長許多裸影。但在另一方面挣轨,如果想在序列中部插入一個元素,用列表就比用矢量劃算得多空民。這些以及其他操作都有不同的執(zhí)行效率刃唐,具體取決于序列的基礎(chǔ)結(jié)構(gòu)是什么羞迷。在設(shè)計階段,我們可以先從一個列表開始画饥。最后調(diào)整性能的時候衔瓮,再根據(jù)情況把它換成矢量。由于抽象是通過繼承器進(jìn)行的抖甘,所以能在兩者方便地切換热鞍,對代碼的影響則顯得微不足道。
最后衔彻,記住集合只是一個用來放置對象的儲藏所薇宠。如果那個儲藏所能滿足我們的所有需要,就完全沒必要關(guān)心它具體是如何實現(xiàn)的(這是大多數(shù)類型對象的一個基本概念)艰额。如果在一個編程環(huán)境中工作澄港,它由于其他因素(比如在Windows下運行,或者由垃圾收集器帶來了開銷)產(chǎn)生了內(nèi)在的開銷柄沮,那么矢量和鏈接列表之間在系統(tǒng)開銷上的差異就或許不是一個大問題回梧。我們可能只需要一種類型的序列。甚至可以想象有一個“完美”的集合抽象祖搓,它能根據(jù)自己的使用方式自動改變基層的實現(xiàn)方式狱意。
1.7.2 單根結(jié)構(gòu)
在面向?qū)ο蟮某绦蛟O(shè)計中,由于C++的引入而顯得尤為突出的一個問題是:所有類最終是否都應(yīng)從單獨一個基礎(chǔ)類繼承拯欧。在Java中(與其他幾乎所有OOP語言一樣)详囤,對這個問題的答案都是肯定的,而且這個終級基礎(chǔ)類的名字很簡單镐作,就是一個“Object”藏姐。這種“單根結(jié)構(gòu)”具有許多方面的優(yōu)點。
單根結(jié)構(gòu)中的所有對象都有一個通用接口该贾,所以它們最終都屬于相同的類型包各。另一種方案(就象C++那樣)是我們不能保證所有東西都屬于相同的基本類型。從向后兼容的角度看靶庙,這一方案可與C模型更好地配合问畅,而且可以認(rèn)為它的限制更少一些。但假期我們想進(jìn)行純粹的面向?qū)ο缶幊塘模敲幢仨殬?gòu)建自己的結(jié)構(gòu)护姆,以期獲得與內(nèi)建到其他OOP語言里的同樣的便利。需添加我們要用到的各種新類庫掏击,還要使用另一些不兼容的接口卵皂。理所當(dāng)然地,這也需要付出額外的精力使新接口與自己的設(shè)計方案配合(可能還需要多重繼承)砚亭。為得到C++額外的“靈活性”灯变,付出這樣的代價值得嗎殴玛?當(dāng)然,如果真的需要——如果早已是C專家添祸,如果對C有難舍的情結(jié)——那么就真的很值得滚粟。但假如你是一名新手,首次接觸這類設(shè)計刃泌,象Java那樣的替換方案也許會更省事一些凡壤。
單根結(jié)構(gòu)中的所有對象(比如所有Java對象)都可以保證擁有一些特定的功能。在自己的系統(tǒng)中耙替,我們知道對每個對象都能進(jìn)行一些基本操作亚侠。一個單根結(jié)構(gòu),加上所有對象都在內(nèi)存堆中創(chuàng)建俗扇,可以極大簡化參數(shù)的傳遞(這在C++里是一個復(fù)雜的概念)硝烂。
利用單根結(jié)構(gòu),我們可以更方便地實現(xiàn)一個垃圾收集器铜幽。與此有關(guān)的必要支持可安裝于基礎(chǔ)類中钢坦,而垃圾收集器可將適當(dāng)?shù)南l(fā)給系統(tǒng)內(nèi)的任何對象。如果沒有這種單根結(jié)構(gòu)啥酱,而且系統(tǒng)通過一個句柄來操縱對象,那么實現(xiàn)垃圾收集器的途徑會有很大的不同厨诸,而且會面臨許多障礙镶殷。
由于運行期的類型信息肯定存在于所有對象中,所以永遠(yuǎn)不會遇到判斷不出一個對象的類型的情況微酬。這對系統(tǒng)級的操作來說顯得特別重要绘趋,比如違例控制;而且也能在程序設(shè)計時獲得更大的靈活性颗管。
但大家也可能產(chǎn)生疑問陷遮,既然你把好處說得這么天花亂墜,為什么C++沒有采用單根結(jié)構(gòu)呢垦江?事實上帽馋,這是早期在效率與控制上權(quán)衡的一種結(jié)果。單根結(jié)構(gòu)會帶來程序設(shè)計上的一些限制比吭。而且更重要的是绽族,它加大了新程序與原有C代碼兼容的難度。盡管這些限制僅在特定的場合會真的造成問題衩藤,但為了獲得最大的靈活程度吧慢,C++最終決定放棄采用單根結(jié)構(gòu)這一做法。而Java不存在上述的問題赏表,它是全新設(shè)計的一種語言检诗,不必與現(xiàn)有的語言保持所謂的“向后兼容”匈仗。所以很自然地,與其他大多數(shù)面向?qū)ο蟮某绦蛟O(shè)計語言一樣逢慌,單根結(jié)構(gòu)在Java的設(shè)計方案中很快就落實下來悠轩。
1.7.3 集合庫與方便使用集合
由于集合是我們經(jīng)常都要用到的一種工具,所以一個集合庫是十分必要的涕癣,它應(yīng)該可以方便地重復(fù)使用哗蜈。這樣一來,我們就可以方便地取用各種集合坠韩,將其插入自己的程序距潘。Java提供了這樣的一個庫,盡管它在Java 1.0和1.1中都顯得非常有限(Java 1.2的集合庫則無疑是一個杰作)只搁。
1.下溯造型與模板/通用性
為了使這些集合能夠重復(fù)使用音比,或者“再生”,Java提供了一種通用類型氢惋,以前曾把它叫作“Object”洞翩。單根結(jié)構(gòu)意味著、所有東西歸根結(jié)底都是一個對象”焰望!所以容納了Object的一個集合實際可以容納任何東西骚亿。這使我們對它的重復(fù)使用變得非常簡便。
為使用這樣的一個集合熊赖,只需添加指向它的對象句柄即可来屠,以后可以通過句柄重新使用對象。但由于集合只能容納Object震鹉,所以在我們向集合里添加對象句柄時俱笛,它會上溯造型成Object,這樣便丟失了它的身份或者標(biāo)識信息传趾。再次使用它的時候迎膜,會得到一個Object句柄,而非指向我們早先置入的那個類型的句柄浆兰。所以怎樣才能歸還它的本來面貌磕仅,調(diào)用早先置入集合的那個對象的有用接口呢?
在這里簸呈,我們再次用到了造型(Cast)宽涌。但這一次不是在分級結(jié)構(gòu)中上溯造型成一種更“通用”的類型。而是下溯造型成一種更“特殊”的類型蝶棋。這種造型方法叫作“下溯造型”(Downcasting)卸亮。舉個例子來說,我們知道在上溯造型的時候玩裙,Circle(圓)屬于Shape(幾何形狀)的一種類型兼贸,所以上溯造型是安全的段直。但我們不知道一個Object到底是Circle還是Shape,所以很難保證下溯造型的安全進(jìn)行溶诞,除非確切地知道自己要操作的是什么鸯檬。
但這也不是絕對危險的,因為假如下溯造型成錯誤的東西螺垢,會得到我們稱為“違例”(Exception)的一種運行期錯誤喧务。我們稍后即會對此進(jìn)行解釋。但在從一個集合提取對象句柄時枉圃,必須用某種方式準(zhǔn)確地記住它們是什么功茴,以保證下溯造型的正確進(jìn)行。
下溯造型和運行期檢查都要求花額外的時間來運行程序孽亲,而且程序員必須付出額外的精力坎穿。既然如此,我們能不能創(chuàng)建一個“智能”集合返劲,令其知道自己容納的類型呢玲昧?這樣做可消除下溯造型的必要以及潛在的錯誤。答案是肯定的篮绿,我們可以采用“參數(shù)化類型”孵延,它們是編譯器能自動定制的類,可與特定的類型配合亲配。例如尘应,通過使用一個參數(shù)化集合,編譯器可對那個集合進(jìn)行定制弃榨,使其只接受Shape,而且只提取Shape梨睁。
參數(shù)化類型是C++一個重要的組成部分鲸睛,這部分是C++沒有單根結(jié)構(gòu)的緣故。在C++中坡贺,用于實現(xiàn)參數(shù)化類型的關(guān)鍵字是template(模板)官辈。Java目前尚未提供參數(shù)化類型,因為由于使用的是單根結(jié)構(gòu)遍坟,所以使用它顯得有些笨拙拳亿。但這并不能保證以后的版本不會實現(xiàn),因為“generic”這個詞已被Java“保留到將來實現(xiàn)”(在Ada語言中愿伴,“generic”被用來實現(xiàn)它的模板)肺魁。Java采取的這種關(guān)鍵字保留機(jī)制其實經(jīng)常讓人摸不著頭腦,很難斷定以后會發(fā)生什么事情隔节。
1.7.4 清除時的困境:由誰負(fù)責(zé)清除鹅经?
每個對象都要求資源才能“生存”寂呛,其中最令人注目的資源是內(nèi)存。如果不再需要使用一個對象瘾晃,就必須將其清除贷痪,以便釋放這些資源,以便其他對象使用蹦误。如果要解決的是非常簡單的問題劫拢,如何清除對象這個問題并不顯得很突出:我們創(chuàng)建對象,在需要的時候調(diào)用它强胰,然后將其清除或者“破壞”舱沧。但在另一方面,我們平時遇到的問題往往要比這復(fù)雜得多哪廓。
舉個例子來說狗唉,假設(shè)我們要設(shè)計一套系統(tǒng),用它管理一個機(jī)場的空中交通(同樣的模型也可能適于管理一個倉庫的貨柜涡真、或者一套影帶出租系統(tǒng)分俯、或者寵物店的寵物房。這初看似乎十分簡單:構(gòu)造一個集合用來容納飛機(jī)哆料,然后創(chuàng)建一架新飛機(jī)缸剪,將其置入集合。對進(jìn)入空中交通管制區(qū)的所有飛機(jī)都如此處理东亦。至于清除杏节,在一架飛機(jī)離開這個區(qū)域的時候把它簡單地刪去即可。
但事情并沒有這么簡單典阵,可能還需要另一套系統(tǒng)來記錄與飛機(jī)有關(guān)的數(shù)據(jù)奋渔。當(dāng)然,和控制器的主要功能不同壮啊,這些數(shù)據(jù)的重要性可能一開始并不顯露出來嫉鲸。例如,這條記錄反映的可能是離開機(jī)場的所有小飛機(jī)的飛行計劃歹啼。所以我們得到了由小飛機(jī)組成的另一個集合玄渗。一旦創(chuàng)建了一個飛機(jī)對象,如果它是一架小飛機(jī)狸眼,那么也必須把它置入這個集合藤树。然后在系統(tǒng)空閑時期,需對這個集合中的對象進(jìn)行一些后臺處理拓萌。
問題現(xiàn)在顯得更復(fù)雜了:如何才能知道什么時間刪除對象呢岁钓?用完對象后,系統(tǒng)的其他某些部分可能仍然要發(fā)揮作用。同樣的問題也會在其他大量場合出現(xiàn)甜紫,而且在程序設(shè)計系統(tǒng)中(如C++)降宅,在用完一個對象之后必須明確地將其刪除,所以問題會變得異常復(fù)雜(注釋⑥)囚霸。
⑥:注意這一點只對內(nèi)存堆里創(chuàng)建的對象成立(用new命令創(chuàng)建的)腰根。但在另一方面,對這兒描述的問題以及其他所有常見的編程問題來說拓型,都要求對象在內(nèi)存堆里創(chuàng)建额嘿。
在Java中,垃圾收集器在設(shè)計時已考慮到了內(nèi)存的釋放問題(盡管這并不包括清除一個對象涉及到的其他方面)劣挫。垃圾收集器“知道”一個對象在什么時候不再使用册养,然后會自動釋放那個對象占據(jù)的內(nèi)存空間。采用這種方式压固,另外加上所有對象都從單個根類Object繼承的事實球拦,而且由于我們只能在內(nèi)存堆中以一種方式創(chuàng)建對象,所以Java的編程要比C++的編程簡單得多帐我。我們只需要作出少量的抉擇坎炼,即可克服原先存在的大量障礙。
2.垃圾收集器對效率及靈活性的影響
既然這是如此好的一種手段拦键,為什么在C++里沒有得到充分的發(fā)揮呢谣光?我們當(dāng)然要為這種編程的方便性付出一定的代價,代價就是運行期的開銷芬为。正如早先提到的那樣萄金,在C++中,我們可在堆棧中創(chuàng)建對象媚朦。在這種情況下氧敢,對象會得以自動清除(但不具有在運行期間隨心所欲創(chuàng)建對象的靈活性)。在堆棧中創(chuàng)建對象是為對象分配存儲空間最有效的一種方式询张,也是釋放那些空間最有效的一種方式孙乖。在內(nèi)存堆(Heap)中創(chuàng)建對象可能要付出昂貴得多的代價。如果總是從同一個基礎(chǔ)類繼承瑞侮,并使所有函數(shù)調(diào)用都具有“同質(zhì)多形”特征的圆,那么也不可避免地需要付出一定的代價鼓拧。但垃圾收集器是一種特殊的問題半火,因為我們永遠(yuǎn)不能確定它什么時候啟動或者要花多長的時間。這意味著在Java程序執(zhí)行期間季俩,存在著一種不連貫的因素钮糖。所以在某些特殊的場合,我們必須避免用它——比如在一個程序的執(zhí)行必須保持穩(wěn)定、連貫的時候(通常把它們叫作“實時程序”店归,盡管并不是所有實時編程問題都要這方面的要求——注釋⑦)阎抒。
⑦:根據(jù)本書一些技術(shù)性讀者的反饋,有一個現(xiàn)成的實時Java系統(tǒng)(www.newmonics.com)確實能夠保證垃圾收集器的效能消痛。
C++語言的設(shè)計者曾經(jīng)向C程序員發(fā)出請求(而且做得非常成功)且叁,不要希望在可以使用C的任何地方,向語言里加入可能對C++的速度或使用造成影響的任何特性秩伞。這個目的達(dá)到了逞带,但代價就是C++的編程不可避免地復(fù)雜起來。Java比C++簡單纱新,但付出的代價是效率以及一定程度的靈活性展氓。但對大多數(shù)程序設(shè)計問題來說,Java無疑都應(yīng)是我們的首選脸爱。