# 1.7 對象的創建和存在時間
從技術角度說,OOP(面向對象程序設計)只是涉及抽象的數據類型、繼承以及多態性,但另一些問題也可能顯得非常重要。本節將就這些問題進行探討。
最重要的問題之一是對象的創建及析構方式。對象需要的數據位于哪兒,如何控制對象的“存在時間”呢?針對這個問題,解決的方案是各異其趣的。C++認為程序的執行效率是最重要的一個問題,所以它允許程序員作出選擇。為獲得最快的運行速度,存儲以及存在時間可在編寫程序時決定,只需將對象放置在棧(有時也叫作自動或定域變量)或者靜態存儲區域即可。這樣便為存儲空間的分配和釋放提供了一個優先級。某些情況下,這種優先級的控制是非常有價值的。然而,我們同時也犧牲了靈活性,因為在編寫程序時,必須知道對象的準確的數量、存在時間、以及類型。如果要解決的是一個較常規的問題,如計算機輔助設計、倉儲管理或者空中交通控制,這一方法就顯得太局限了。
第二個方法是在一個內存池中動態創建對象,該內存池亦叫“堆”或者“內存堆”。若采用這種方式,除非進入運行期,否則根本不知道到底需要多少個對象,也不知道它們的存在時間有多長,以及準確的類型是什么。這些參數都在程序正式運行時才決定的。若需一個新對象,只需在需要它的時候在內存堆里簡單地創建它即可。由于存儲空間的管理是運行期間動態進行的,所以在內存堆里分配存儲空間的時間比在棧里創建的時間長得多(在棧里創建存儲空間一般只需要一個簡單的指令,將棧指針向下或向下移動即可)。由于動態創建方法使對象本來就傾向于復雜,所以查找存儲空間以及釋放它所需的額外開銷不會為對象的創建造成明顯的影響。除此以外,更大的靈活性對于常規編程問題的解決是至關重要的。
C++允許我們決定是在寫程序時創建對象,還是在運行期間創建,這種控制方法更加靈活。大家或許認為既然它如此靈活,那么無論如何都應在內存堆里創建對象,而不是在棧中創建。但還要考慮另外一個問題,亦即對象的“存在時間”或者“生存時間”(Lifetime)。若在棧或者靜態存儲空間里創建一個對象,編譯器會判斷對象的持續時間有多長,到時會自動“析構”或者“清除”它。程序員可用兩種方法來析構一個對象:用程序化的方式決定何時析構對象,或者利用由運行環境提供的一種“垃圾收集器”特性,自動尋找那些不再使用的對象,并將其清除。當然,垃圾收集器顯得方便得多,但要求所有應用程序都必須容忍垃圾收集器的存在,并能默許隨垃圾收集帶來的額外開銷。但這并不符合C++語言的設計宗旨,所以未能包括到C++里。但Java確實提供了一個垃圾收集器(Smalltalk也有這樣的設計;盡管Delphi默認為沒有垃圾收集器,但可選擇安裝;而C++亦可使用一些由其他公司開發的垃圾收集產品)。
本節剩下的部分將討論操縱對象時要考慮的另一些因素。
## 1.7.1 集合與迭代器
針對一個特定問題的解決,如果事先不知道需要多少個對象,或者它們的持續時間有多長,那么也不知道如何保存那些對象。既然如此,怎樣才能知道那些對象要求多少空間呢?事先上根本無法提前知道,除非進入運行期。
在面向對象的設計中,大多數問題的解決辦法似乎都有些輕率——只是簡單地創建另一種類型的對象。用于解決特定問題的新型對象容納了指向其他對象的引用。當然,也可以用數組來做同樣的事情,那是大多數語言都具有的一種功能。但不能只看到這一點。這種新對象通常叫作“集合”(亦叫作一個“容器”,但AWT在不同的場合應用了這個術語,所以本書將一直沿用“集合”的稱呼。在需要的時候,集合會自動擴充自己,以便適應我們在其中置入的任何東西。所以我們事先不必知道要在一個集合里容下多少東西。只需創建一個集合,以后的工作讓它自己負責好了。
幸運的是,設計優良的OOP語言都配套提供了一系列集合。在C++中,它們是以“標準模板庫”(STL)的形式提供的。Object Pascal用自己的“可視組件庫”(VCL)提供集合。Smalltalk提供了一套非常完整的集合。而Java也用自己的標準庫提供了集合。在某些庫中,一個常規集合便可滿足人們的大多數要求;而在另一些庫中(特別是C++的庫),則面向不同的需求提供了不同類型的集合。例如,可以用一個向量統一對所有元素的訪問方式;一個鏈接列表則用于保證所有元素的插入統一。所以我們能根據自己的需要選擇適當的類型。其中包括集、隊列、散列表、樹、棧等等。
所有集合都提供了相應的讀寫功能。將某樣東西置入集合時,采用的方式是十分明顯的。有一個叫作“推”(Push)、“添加”(Add)或其他類似名字的函數用于做這件事情。但將數據從集合中取出的時候,方式卻并不總是那么明顯。如果是一個數組形式的實體,比如一個向量(`Vector`),那么也許能用索引運算符或函數。但在許多情況下,這樣做往往會無功而返。此外,單選定函數的功能是非常有限的。如果想對集合中的一系列元素進行操縱或比較,而不是僅僅面向一個,這時又該怎么辦呢?
辦法就是使用一個“迭代器”(`Iterator`),它屬于一種對象,負責選擇集合內的元素,并把它們提供給迭代器的用戶。作為一個類,它也提供了一級抽象。利用這一級抽象,可將集合細節與用于訪問那個集合的代碼隔離開。通過迭代器的作用,集合被抽象成一個簡單的序列。迭代器允許我們遍歷那個序列,同時毋需關心基礎結構是什么——換言之,不管它是一個向量、一個鏈接列表、一個棧,還是其他什么東西。這樣一來,我們就可以靈活地改變基礎數據,不會對程序里的代碼造成干擾。Java最開始(在1.0和1.1版中)提供的是一個標準迭代器,名為`Enumeration`(枚舉),為它的所有集合類提供服務。Java 1.2新增一個更復雜的集合庫,其中包含了一個名為`Iterator`的迭代器,可以做比老式的`Enumeration`更多的事情。
從設計角度出發,我們需要的是一個全功能的序列。通過對它的操縱,應該能解決自己的問題。如果一種類型的序列即可滿足我們的所有要求,那么完全沒有必要再換用不同的類型。有兩方面的原因促使我們需要對集合作出選擇。首先,集合提供了不同的接口類型以及外部行為。棧的接口與行為與隊列的不同,而隊列的接口與行為又與一個集(Set)或列表的不同。利用這個特征,我們解決問題時便有更大的靈活性。
其次,不同的集合在進行特定操作時往往有不同的效率。最好的例子便是向量(`Vector`)和列表(`List`)的區別。它們都屬于簡單的序列,擁有完全一致的接口和外部行為。但在執行一些特定的任務時,需要的開銷卻是完全不同的。對向量內的元素進行的隨機訪問(存取)是一種常時操作;無論我們選擇的選擇是什么,需要的時間量都是相同的。但在一個鏈接列表中,若想到處移動,并隨機挑選一個元素,就需付出“慘重”的代價。而且假設某個元素位于列表較遠的地方,找到它所需的時間也會長許多。但在另一方面,如果想在序列中部插入一個元素,用列表就比用向量劃算得多。這些以及其他操作都有不同的執行效率,具體取決于序列的基礎結構是什么。在設計階段,我們可以先從一個列表開始。最后調整性能的時候,再根據情況把它換成向量。由于抽象是通過迭代器進行的,所以能在兩者方便地切換,對代碼的影響則顯得微不足道。
最后,記住集合只是一個用來放置對象的儲藏所。如果那個儲藏所能滿足我們的所有需要,就完全沒必要關心它具體是如何實現的(這是大多數類型對象的一個基本概念)。如果在一個編程環境中工作,它由于其他因素(比如在Windows下運行,或者由垃圾收集器帶來了開銷)產生了內在的開銷,那么向量和鏈接列表之間在系統開銷上的差異就或許不是一個大問題。我們可能只需要一種類型的序列。甚至可以想象有一個“完美”的集合抽象,它能根據自己的使用方式自動改變基層的實現方式。
## 1.7.2 單根結構
在面向對象的程序設計中,由于C++的引入而顯得尤為突出的一個問題是:所有類最終是否都應從單獨一個基類繼承。在Java中(與其他幾乎所有OOP語言一樣),對這個問題的答案都是肯定的,而且這個終級基類的名字很簡單,就是一個`Object`。這種“單根結構”具有許多方面的優點。
單根結構中的所有對象都有一個通用接口,所以它們最終都屬于相同的類型。另一種方案(就象C++那樣)是我們不能保證所有東西都屬于相同的基本類型。從向后兼容的角度看,這一方案可與C模型更好地配合,而且可以認為它的限制更少一些。但假期我們想進行純粹的面向對象編程,那么必須構建自己的結構,以期獲得與內建到其他OOP語言里的同樣的便利。需添加我們要用到的各種新類庫,還要使用另一些不兼容的接口。理所當然地,這也需要付出額外的精力使新接口與自己的設計模式配合(可能還需要多重繼承)。為得到C++額外的“靈活性”,付出這樣的代價值得嗎?當然,如果真的需要——如果早已是C專家,如果對C有難舍的情結——那么就真的很值得。但假如你是一名新手,首次接觸這類設計,象Java那樣的替換方案也許會更省事一些。
單根結構中的所有對象(比如所有Java對象)都可以保證擁有一些特定的功能。在自己的系統中,我們知道對每個對象都能進行一些基本操作。一個單根結構,加上所有對象都在內存堆中創建,可以極大簡化參數的傳遞(這在C++里是一個復雜的概念)。
利用單根結構,我們可以更方便地實現一個垃圾收集器。與此有關的必要支持可安裝于基類中,而垃圾收集器可將適當的消息發給系統內的任何對象。如果沒有這種單根結構,而且系統通過一個引用來操縱對象,那么實現垃圾收集器的途徑會有很大的不同,而且會面臨許多障礙。
由于運行期的類型信息肯定存在于所有對象中,所以永遠不會遇到判斷不出一個對象的類型的情況。這對系統級的操作來說顯得特別重要,比如異常控制;而且也能在程序設計時獲得更大的靈活性。
但大家也可能產生疑問,既然你把好處說得這么天花亂墜,為什么C++沒有采用單根結構呢?事實上,這是早期在效率與控制上權衡的一種結果。單根結構會帶來程序設計上的一些限制。而且更重要的是,它加大了新程序與原有C代碼兼容的難度。盡管這些限制僅在特定的場合會真的造成問題,但為了獲得最大的靈活程度,C++最終決定放棄采用單根結構這一做法。而Java不存在上述的問題,它是全新設計的一種語言,不必與現有的語言保持所謂的“向后兼容”。所以很自然地,與其他大多數面向對象的程序設計語言一樣,單根結構在Java的設計模式中很快就落實下來。
## 1.7.3 集合庫與方便使用集合
由于集合是我們經常都要用到的一種工具,所以一個集合庫是十分必要的,它應該可以方便地重復使用。這樣一來,我們就可以方便地取用各種集合,將其插入自己的程序。Java提供了這樣的一個庫,盡管它在Java 1.0和1.1中都顯得非常有限(Java 1.2的集合庫則無疑是一個杰作)。
(1)向下轉換與模板/通用性
為了使這些集合能夠重復使用,或者“復用”,Java提供了一種通用類型,以前曾把它叫作`Object`。單根結構意味著、所有東西歸根結底都是一個對象”!所以容納了`Object`的一個集合實際可以容納任何東西。這使我們對它的重復使用變得非常簡便。
為使用這樣的一個集合,只需添加指向它的對象引用即可,以后可以通過引用重新使用對象。但由于集合只能容納`Object`,所以在我們向集合里添加對象引用時,它會向上轉換成`Object`,這樣便丟失了它的身份或者標識信息。再次使用它的時候,會得到一個`Object`引用,而非指向我們早先置入的那個類型的引用。所以怎樣才能歸還它的本來面貌,調用早先置入集合的那個對象的有用接口呢?
在這里,我們再次用到了轉換(Cast)。但這一次不是在分級結構中向上轉換成一種更“通用”的類型。而是向下轉換成一種更“特殊”的類型。這種轉換方法叫作“向下轉換”(Downcasting)。舉個例子來說,我們知道在向上轉換的時候,`Circle`(圓)屬于`Shape`(幾何形狀)的一種類型,所以向上轉換是安全的。但我們不知道一個`Object`到底是`Circle`還是`Shape`,所以很難保證向下轉換的安全進行,除非確切地知道自己要操作的是什么。
但這也不是絕對危險的,因為假如向下轉換成錯誤的東西,會得到我們稱為“異常”(`Exception`)的一種運行期錯誤。我們稍后即會對此進行解釋。但在從一個集合提取對象引用時,必須用某種方式準確地記住它們是什么,以保證向下轉換的正確進行。
向下轉換和運行期檢查都要求花額外的時間來運行程序,而且程序員必須付出額外的精力。既然如此,我們能不能創建一個“智能”集合,令其知道自己容納的類型呢?這樣做可消除向下轉換的必要以及潛在的錯誤。答案是肯定的,我們可以采用“參數化類型”,它們是編譯器能自動定制的類,可與特定的類型配合。例如,通過使用一個參數化集合,編譯器可對那個集合進行定制,使其只接受`Shape`,而且只提取`Shape`。
參數化類型是C++一個重要的組成部分,這部分是C++沒有單根結構的緣故。在C++中,用于實現參數化類型的關鍵字是`template`(模板)。Java目前尚未提供參數化類型,因為由于使用的是單根結構,所以使用它顯得有些笨拙。但這并不能保證以后的版本不會實現,因為`generic`這個詞已被Java“保留到將來實現”(在Ada語言中,`generic`被用來實現它的模板)。Java采取的這種關鍵字保留機制其實經常讓人摸不著頭腦,很難斷定以后會發生什么事情。
## 1.7.4 清除時的困境:由誰負責清除?
每個對象都要求資源才能“生存”,其中最令人注目的資源是內存。如果不再需要使用一個對象,就必須將其清除,以便釋放這些資源,以便其他對象使用。如果要解決的是非常簡單的問題,如何清除對象這個問題并不顯得很突出:我們創建對象,在需要的時候調用它,然后將其清除或者“析構”。但在另一方面,我們平時遇到的問題往往要比這復雜得多。
舉個例子來說,假設我們要設計一套系統,用它管理一個機場的空中交通(同樣的模型也可能適于管理一個倉庫的貨柜、或者一套影帶出租系統、或者寵物店的寵物房。這初看似乎十分簡單:構造一個集合用來容納飛機,然后創建一架新飛機,將其置入集合。對進入空中交通管制區的所有飛機都如此處理。至于清除,在一架飛機離開這個區域的時候把它簡單地刪去即可。
但事情并沒有這么簡單,可能還需要另一套系統來記錄與飛機有關的數據。當然,和控制器的主要功能不同,這些數據的重要性可能一開始并不顯露出來。例如,這條記錄反映的可能是離開機場的所有小飛機的飛行計劃。所以我們得到了由小飛機組成的另一個集合。一旦創建了一個飛機對象,如果它是一架小飛機,那么也必須把它置入這個集合。然后在系統空閑時期,需對這個集合中的對象進行一些后臺處理。
問題現在顯得更復雜了:如何才能知道什么時間刪除對象呢?用完對象后,系統的其他某些部分可能仍然要發揮作用。同樣的問題也會在其他大量場合出現,而且在程序設計系統中(如C++),在用完一個對象之后必須明確地將其刪除,所以問題會變得異常復雜(注釋⑥)。
⑥:注意這一點只對內存堆里創建的對象成立(用new命令創建的)。但在另一方面,對這兒描述的問題以及其他所有常見的編程問題來說,都要求對象在內存堆里創建。
在Java中,垃圾收集器在設計時已考慮到了內存的釋放問題(盡管這并不包括清除一個對象涉及到的其他方面)。垃圾收集器“知道”一個對象在什么時候不再使用,然后會自動釋放那個對象占據的內存空間。采用這種方式,另外加上所有對象都從單個根類`Object`繼承的事實,而且由于我們只能在內存堆中以一種方式創建對象,所以Java的編程要比C++的編程簡單得多。我們只需要作出少量的抉擇,即可克服原先存在的大量障礙。
(2)垃圾收集器對效率及靈活性的影響
既然這是如此好的一種手段,為什么在C++里沒有得到充分的發揮呢?我們當然要為這種編程的方便性付出一定的代價,代價就是運行期的開銷。正如早先提到的那樣,在C++中,我們可在棧中創建對象。在這種情況下,對象會得以自動清除(但不具有在運行期間隨心所欲創建對象的靈活性)。在棧中創建對象是為對象分配存儲空間最有效的一種方式,也是釋放那些空間最有效的一種方式。在內存堆(Heap)中創建對象可能要付出昂貴得多的代價。如果總是從同一個基類繼承,并使所有函數調用都具有“同質多態”特征,那么也不可避免地需要付出一定的代價。但垃圾收集器是一種特殊的問題,因為我們永遠不能確定它什么時候啟動或者要花多長的時間。這意味著在Java程序執行期間,存在著一種不連貫的因素。所以在某些特殊的場合,我們必須避免用它——比如在一個程序的執行必須保持穩定、連貫的時候(通常把它們叫作“實時程序”,盡管并不是所有實時編程問題都要這方面的要求——注釋⑦)。
⑦:根據本書一些技術性讀者的反饋,有一個現成的實時Java系統(`www.newmonics.com`)確實能夠保證垃圾收集器的效能。
C++語言的設計者曾經向C程序員發出請求(而且做得非常成功),不要希望在可以使用C的任何地方,向語言里加入可能對C++的速度或使用造成影響的任何特性。這個目的達到了,但代價就是C++的編程不可避免地復雜起來。Java比C++簡單,但付出的代價是效率以及一定程度的靈活性。但對大多數程序設計問題來說,Java無疑都應是我們的首選。
- Java 編程思想
- 寫在前面的話
- 引言
- 第1章 對象入門
- 1.1 抽象的進步
- 1.2 對象的接口
- 1.3 實現方案的隱藏
- 1.4 方案的重復使用
- 1.5 繼承:重新使用接口
- 1.6 多態對象的互換使用
- 1.7 對象的創建和存在時間
- 1.8 異常控制:解決錯誤
- 1.9 多線程
- 1.10 永久性
- 1.11 Java和因特網
- 1.12 分析和設計
- 1.13 Java還是C++
- 第2章 一切都是對象
- 2.1 用引用操縱對象
- 2.2 所有對象都必須創建
- 2.3 絕對不要清除對象
- 2.4 新建數據類型:類
- 2.5 方法、參數和返回值
- 2.6 構建Java程序
- 2.7 我們的第一個Java程序
- 2.8 注釋和嵌入文檔
- 2.9 編碼樣式
- 2.10 總結
- 2.11 練習
- 第3章 控制程序流程
- 3.1 使用Java運算符
- 3.2 執行控制
- 3.3 總結
- 3.4 練習
- 第4章 初始化和清除
- 4.1 用構造器自動初始化
- 4.2 方法重載
- 4.3 清除:收尾和垃圾收集
- 4.4 成員初始化
- 4.5 數組初始化
- 4.6 總結
- 4.7 練習
- 第5章 隱藏實現過程
- 5.1 包:庫單元
- 5.2 Java訪問指示符
- 5.3 接口與實現
- 5.4 類訪問
- 5.5 總結
- 5.6 練習
- 第6章 類復用
- 6.1 組合的語法
- 6.2 繼承的語法
- 6.3 組合與繼承的結合
- 6.4 到底選擇組合還是繼承
- 6.5 protected
- 6.6 累積開發
- 6.7 向上轉換
- 6.8 final關鍵字
- 6.9 初始化和類裝載
- 6.10 總結
- 6.11 練習
- 第7章 多態性
- 7.1 向上轉換
- 7.2 深入理解
- 7.3 覆蓋與重載
- 7.4 抽象類和方法
- 7.5 接口
- 7.6 內部類
- 7.7 構造器和多態性
- 7.8 通過繼承進行設計
- 7.9 總結
- 7.10 練習
- 第8章 對象的容納
- 8.1 數組
- 8.2 集合
- 8.3 枚舉器(迭代器)
- 8.4 集合的類型
- 8.5 排序
- 8.6 通用集合庫
- 8.7 新集合
- 8.8 總結
- 8.9 練習
- 第9章 異常差錯控制
- 9.1 基本異常
- 9.2 異常的捕獲
- 9.3 標準Java異常
- 9.4 創建自己的異常
- 9.5 異常的限制
- 9.6 用finally清除
- 9.7 構造器
- 9.8 異常匹配
- 9.9 總結
- 9.10 練習
- 第10章 Java IO系統
- 10.1 輸入和輸出
- 10.2 增添屬性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File類
- 10.5 IO流的典型應用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 壓縮
- 10.9 對象序列化
- 10.10 總結
- 10.11 練習
- 第11章 運行期類型識別
- 11.1 對RTTI的需要
- 11.2 RTTI語法
- 11.3 反射:運行期類信息
- 11.4 總結
- 11.5 練習
- 第12章 傳遞和返回對象
- 12.1 傳遞引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只讀類
- 12.5 總結
- 12.6 練習
- 第13章 創建窗口和程序片
- 13.1 為何要用AWT?
- 13.2 基本程序片
- 13.3 制作按鈕
- 13.4 捕獲事件
- 13.5 文本字段
- 13.6 文本區域
- 13.7 標簽
- 13.8 復選框
- 13.9 單選鈕
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 視窗化應用
- 13.16 新型AWT
- 13.17 Java 1.1用戶接口API
- 13.18 可視編程和Beans
- 13.19 Swing入門
- 13.20 總結
- 13.21 練習
- 第14章 多線程
- 14.1 反應靈敏的用戶界面
- 14.2 共享有限的資源
- 14.3 堵塞
- 14.4 優先級
- 14.5 回顧runnable
- 14.6 總結
- 14.7 練習
- 第15章 網絡編程
- 15.1 機器的標識
- 15.2 套接字
- 15.3 服務多個客戶
- 15.4 數據報
- 15.5 一個Web應用
- 15.6 Java與CGI的溝通
- 15.7 用JDBC連接數據庫
- 15.8 遠程方法
- 15.9 總結
- 15.10 練習
- 第16章 設計模式
- 16.1 模式的概念
- 16.2 觀察器模式
- 16.3 模擬垃圾回收站
- 16.4 改進設計
- 16.5 抽象的應用
- 16.6 多重分發
- 16.7 訪問器模式
- 16.8 RTTI真的有害嗎
- 16.9 總結
- 16.10 練習
- 第17章 項目
- 17.1 文字處理
- 17.2 方法查找工具
- 17.3 復雜性理論
- 17.4 總結
- 17.5 練習
- 附錄A 使用非JAVA代碼
- 附錄B 對比C++和Java
- 附錄C Java編程規則
- 附錄D 性能
- 附錄E 關于垃圾收集的一些話
- 附錄F 推薦讀物