1. 程式人生 > >Think IN JAVA 第一章物件入門

Think IN JAVA 第一章物件入門

第1章 物件入門
“為什麼面向物件的程式設計會在軟體開發領域造成如此震憾的影響?”
面向物件程式設計(OOP)具有多方面的吸引力。對管理人員,它實現了更快和更廉價的開發與維護過程。對分析與設計人員,建模處理變得更加簡單,能生成清晰、易於維護的設計方案。對程式設計師,物件模型顯得如此高雅和淺顯。此外,面向物件工具以及庫的巨大威力使程式設計成為一項更使人愉悅的任務。每個人都可從中獲益,至少表面如此。

1.1 抽象的進步
所有程式語言的最終目的都是提供一種“抽象”方法。一種較有爭議的說法是:解決問題的複雜程度直接取決於抽象的種類及質量。這兒的“種類”是指準備對什麼進行“抽象”?組合語言是對基礎機器的少量抽象。後來的許多“命令式”語言(如FORTRAN,BASIC和C)是對組合語言的一種抽象。與組合語言相比,這些語言已有了長足的進步,但它們的抽象原理依然要求我們著重考慮計算機的結構,而非考慮問題本身的結構。
Alan Kay總結了Smalltalk的五大基本特徵。這是第一種成功的面向物件程式設計語言,也是Java的基礎語言。通過這些特徵,我們可理解“純粹”的面向物件程式設計方法是什麼樣的:
(1) 所有東西都是物件。可將物件想象成一種新型變數;它儲存著資料,但可要求它對自身進行操作。
(2) 程式是一大堆物件的組合;通過訊息傳遞,各物件知道自己該做些什麼。為了向物件發出請求,需向那個物件“傳送一條訊息”。更具體地講,可將訊息想象為一個呼叫請求,它呼叫的是從屬於目標物件的一個子例程或函式。
(3) 每個物件都有自己的儲存空間,可容納其他物件。或者說,通過封裝現有物件,可製作出新型物件。
(4) 每個物件都有一種型別。根據語法,每個物件都是某個“類”的一個“例項”。其中,“類”(Class)是“型別”(Type)的同義詞。一個類最重要的特徵就是“能將什麼訊息發給它?”。
(5) 同一類所有物件都能接收相同的訊息。這實際是別有含義的一種說法,大家不久便能理解。由於型別為“圓”(Circle)的一個物件也屬於型別為“形狀”(Shape)的一個物件,所以一個圓完全能接收形狀訊息。這意味著可讓程式程式碼統一指揮“形狀”,令其自動控制所有符合“形狀”描述的物件,其中自然包括“圓”。這一特性稱為物件的“可替換性”,是OOP最重要的概念之一。

1.2 物件的介面
亞里士多德或許是認真研究“型別”概念的第一人,他曾談及“魚類和鳥類”的問題。有些人進行了進一步的區分,他們強調“型別”決定了介面,而“類”是那個介面的一種特殊實現方式。我們向物件發出的請求是通過它的“介面”(Interface)定義的,物件的“型別”或“類”則規定了它的介面形式。“型別”與“介面”的等價或對應關係是面向物件程式設計的基礎。

1.3 實現方案的隱藏
略。
1.4 方案的重複使用
建立並測試好一個類後,它應(從理想的角度)代表一個有用的程式碼單位。但並不象許多人希望的那樣,這種重複使用的能力並不容易實現;它要求較多的經驗以及洞察力,這樣才能設計出一個好的方案,才有可能重複使用。許多人認為程式碼或設計方案的重複使用是面向物件的程式設計提供的最偉大的一種槓桿。物件的組織具有極大的靈活性。新類的“成員物件”通常設為“私有”(Private),使用這個類的客戶程式設計師不能訪問它們。這樣一來,我們可在不干擾客戶程式碼的前提下,從容地修改那些成員。也可以在“執行期”更改成員,這進一步增大了靈活性。後面要講到的“繼承”並不具備這種靈活性,因為編譯器必須對通過繼承建立的類加以限制。

1.5 繼承:重新使用介面
就其本身來說,物件的概念可為我們帶來極大的便利。它在概念上允許我們將各式各樣資料和功能封裝到一起。這樣便可恰當表達“問題空間”的概念,不用刻意遵照基礎機器的表達方式。在程式設計語言中,這些概念則反映為具體的資料型別(使用class關鍵字)。
在繼承過程中,若原始類(正式名稱叫作基礎類、超類或父類)發生了變化,修改過的“克隆”類(正式名稱叫作繼承類或者子類)也會反映出這種變化。在Java語言中,繼承是通過extends關鍵字實現的。使用繼承時,相當於建立了一個新類。這個新類不僅包含了現有型別的所有成員(儘管private成員被隱藏起來,且不能訪問),但更重要的是,它複製了基礎類的介面。也就是說,可向基礎類的物件傳送的所有訊息亦可原樣發給衍生類的物件。這意味著衍生類具有與基礎類相同的型別!有兩種做法可將新得的衍生類與原來的基礎類區分開。第一種做法十分簡單:為衍生類新增新函式(功能)。這些新函式並非基礎類介面的一部分。進行這種處理時,一般都是意識到基礎類不能滿足我們的要求,所以需要新增更多的函式。這是一種最簡單、最基本的繼承用法,大多數時候都可完美地解決我們的問題。

1.5.1 改善基礎類(override覆蓋)
儘管extends關鍵字暗示著我們要為介面“擴充套件”新功能,但實情並非肯定如此。為區分我們的新類,第二個辦法是改變基礎類一個現有函式的行為。我們將其稱作“改善”那個函式。
為改善一個函式,只需為衍生類的函式建立一個新定義即可。我們的目標是:“儘管使用的函式介面未變,但它的新版本具有不同的表現”。

1.6 多形物件的互換使用
通常,繼承最終會以建立一系列類收場,所有類都建立在統一的介面基礎上。
對這樣的一系列類,我們要進行的一項重要處理就是將衍生類的物件當作基礎類的一個物件對待。這一點是非常重要的,因為它意味著我們只需編寫單一的程式碼,令其忽略型別的特定細節,只與基礎類打交道。這樣一來,那些程式碼就可與型別資訊分開。所以更易編寫,也更易理解。此外,若通過繼承增添了一種新型別,如“三角形”,那麼我們為“幾何形狀”新型別編寫的程式碼會象在舊型別裡一樣良好地工作。所以說程式具備了“擴充套件能力”,具有“擴充套件性”。
此時,一個Circle(圓)控制代碼傳遞給一個本來期待Shape(形狀)控制代碼的函式。由於圓是一種幾何形狀,所以doStuff()能正確地進行處理。也就是說,凡是doStuff()能發給一個Shape的訊息,Circle也能接收。所以這樣做是安全的,不會造成錯誤。
我們將這種把衍生型別當作它的基本型別處理的過程叫作“Upcasting”(上溯造型)。其中,“cast”(造型)是指根據一個現成的模型建立;而“Up”(向上)表明繼承的方向是從“上面”來的——即基礎類位於頂部,而衍生類在下方展開。所以,根據基礎類進行造型就是一個從上面繼承的過程,即“Upcasting”。
在面向物件的程式裡,通常都要用到上溯造型技術。這是避免去調查準確型別的一個好辦法。

1.7.1 集合與繼承器(迭代器)
這種新物件通常叫作“集合”(亦叫作一個“容器”)。在需要的時候,集合會自動擴充自己,以便適應我們在其中置入的任何東西。所以我們事先不必知道要在一個集合裡容下多少東西。只需建立一個集合,以後的工作讓它自己負責好了。
所有集合都提供了相應的讀寫功能。將某樣東西置入集合時,採用的方式是十分明顯的。有一個叫作“推”(Push)、“新增”(Add)或其他類似名字的函式用於做這件事情。但將資料從集合中取出的時候,方式卻並不總是那麼明顯。辦法就是使用一個“繼續器”(Iterator),它屬於一種物件,負責選擇集合內的元素,並把它們提供給繼承器的使用者。

1.7.2 單根結構
在面向物件的程式設計中,由於C++的引入而顯得尤為突出的一個問題是:所有類最終是否都應從單獨一個基礎類繼承。在Java中(與其他幾乎所有OOP語言一樣),對這個問題的答案都是肯定的,而且這個終級基礎類的名字很簡單,就是一個“Object”。這種“單根結構”具有許多方面的優點。單根結構中的所有物件都有一個通用介面,所以它們最終都屬於相同的型別。

  1. 下溯造型
    為了使這些集合能夠重複使用,或者“再生”,Java提供了一種通用型別,以前曾把它叫作“Object”。單根結構意味著、所有東西歸根結底都是一個物件”!所以容納了Object的一個集合實際可以容納任何東西。這使我們對它的重複使用變得非常簡便。
    為使用這樣的一個集合,只需新增指向它的物件控制代碼即可,以後可以通過控制代碼重新使用物件。但由於集合只能容納Object,所以在我們向集合裡新增物件控制代碼時,它會上溯造型成Object,這樣便丟失了它的身份或者標識資訊。再次使用它的時候,會得到一個Object控制代碼,而非指向我們早先置入的那個型別的控制代碼。所以怎樣才能歸還它的本來面貌,呼叫早先置入集合的那個物件的有用介面呢?
    在這裡,我們再次用到了造型(Cast)。但這一次不是在分級結構中上溯造型成一種更“通用”的型別。而是下溯造型成一種更“特殊”的型別。這種造型方法叫作“下溯造型”(Downcasting)。舉個例子來說,我們知道在上溯造型的時候,Circle(圓)屬於Shape(幾何形狀)的一種型別,所以上溯造型是安全的。但我們不知道一個Object到底是Circle還是Shape,所以很難保證下溯造型的安全進行,除非確切地知道自己要操作的是什麼。
    但這也不是絕對危險的,因為假如下溯造型成錯誤的東西,會得到我們稱為“違例”(Exception)的一種執行期錯誤。我們稍後即會對此進行解釋。但在從一個集合提取物件控制代碼時,必須用某種方式準確地記住它們是什麼,以保證下溯造型的正確進行。下溯造型和執行期檢查都要求花額外的時間來執行程式,而且程式設計師必須付出額外的精力。既然如此,我們能不能建立一個“智慧”集合,令其知道自己容納的型別呢?這樣做可消除下溯造型的必要以及潛在的錯誤。答案是肯定的,我們可以採用“引數化型別”,它們是編譯器能自動定製的類,可與特定的型別配合。例如,通過使用一個引數化集合,編譯器可對那個集合進行定製,使其只接受Shape,而且只提取Shape。

1.9 多執行緒
在計算機程式設計中,一個基本的概念就是同時對多個任務加以控制。在一個程式中,這些獨立執行的片斷叫作“執行緒”(Thread),利用它程式設計的概念就叫作“多執行緒處理”。多執行緒處理一個常見的例子就是使用者介面。利用執行緒,使用者可按下一個按鈕,然後程式會立即作出響應,而不是讓使用者等待程式完成了當前任務以後才開始響應。解決這個問題,對那些可共享的資源來說(比如印表機),它們在使用期間必須進入鎖定狀態。所以一個執行緒可將資源鎖定,在完成了它的任務後,再解開(釋放)這個鎖,使其他執行緒可以接著使用同樣的資源。
Java的多執行緒機制已內建到語言中,這使一個可能較複雜的問題變得簡單起來。對多執行緒處理的支援是在物件這一級支援的,所以一個執行執行緒可表達為一個物件。Java也提供了有限的資源鎖定方案。它能鎖定任何物件佔用的記憶體(記憶體實際是多種共享資源的一種),所以同一時間只能有一個執行緒使用特定的記憶體空間。為達到這個目的,需要使用synchronized關鍵字。其他型別的資源必須由程式設計師明確鎖定,這通常要求程式設計師建立一個物件,用它代表一把鎖,所有執行緒在訪問那個資源時都必須檢查這把鎖。

1. 客戶機/伺服器計算
客戶機/伺服器系統的基本思想是我們能在一個統一的地方集中存放資訊資源。一般將資料集中儲存在某個資料庫中,根據其他人或者機器的請求將資訊投遞給對方。客戶機/伺服器概述的一個關鍵在於資訊是“集中存放”的。所以我們能方便地更改資訊,然後將修改過的資訊發放給資訊的消費者。將各種元素集中到一起,資訊倉庫、用於投遞資訊的軟體以及資訊及軟體所在的那臺機器,它們聯合起來便叫作“伺服器”(Server)。而對那些駐留在遠端機器上的軟體,它們需要與伺服器通訊,取回資訊,進行適當的處理,然後在遠端機器上顯示出來,這些就叫作“客戶”(Client)。
這樣看來,客戶機/伺服器的基本概念並不複雜。這裡要注意的一個主要問題是單個伺服器需要同時向多個客戶提供服務。在這一機制中,通常少不了一套資料庫管理系統,使設計人員能將資料佈局封裝到表格中,以獲得最優的使用。除此以外,系統經常允許客戶將新資訊插入一個伺服器。這意味著必須確保客戶的新資料不會與其他客戶的新資料衝突,或者說需要保證那些資料在加入資料庫的時候不會丟失(用資料庫的術語來說,這叫作“事務處理”)。客戶軟體發生了改變之後,它們必須在客戶機器上構建、除錯以及安裝。所有這些會使問題變得比我們一般想象的複雜得多。另外,對多種型別的計算機和作業系統的支援也是一個大問題。最後,效能的問題顯得尤為重要:可能會有數百個客戶同時向伺服器發出請求。所以任何微小的延誤都是不能忽視的。為儘可能緩解潛伏的問題,程式設計師需要謹慎地分散任務的處理負擔。一般可以考慮讓客戶機負擔部分處理任務,但有時亦可分派給伺服器所在地的其他機器,那些機器亦叫作“中介軟體”(中介軟體也用於改進對系統的維護)。

  1. Web是一個巨大的伺服器
    Web實際就是一套規模巨大的客戶機/伺服器系統。但它的情況要複雜一些,因為所有伺服器和客戶都同時存在於單個網路上面。但我們沒必要了解更進一步的細節,因為唯一要關心的就是一次建立同一個伺服器的連線,並同它打交道(即使可能要在全世界的範圍內搜尋正確的伺服器)。
    最開始的時候,這是一個簡單的單向操作過程。我們向一個伺服器發出請求,它向我們回傳一個檔案,由於本機的瀏覽器軟體(亦即“客戶”或“客戶程式”)負責解釋和格式化,並在我們面前的螢幕上正確地顯示出來。但人們不久就不滿足於只從一個伺服器傳遞網頁。他們希望獲得完全的客戶機/伺服器能力,使客戶(程式)也能反饋一些資訊到伺服器。比如希望對伺服器上的資料庫進行檢索,向伺服器新增新資訊,或者下一份訂單等等(這也提供了比以前的系統更高的安全要求)。在Web的發展過程中,我們可以很清晰地看出這些令人心喜的變化。
    Web瀏覽器的發展終於邁出了重要的一步:某個資訊可在任何型別的計算機上顯示出來,毋需任何改動。然而,瀏覽器仍然顯得很原始,在使用者迅速增多的要求面前顯得有些力不從心。它們的互動能力不夠強,而且對伺服器和因特網都造成了一定程度的干擾。這是由於每次採取一些要求程式設計的操作時,必須將資訊反饋回伺服器,在伺服器那一端進行處理。所以完全可能需要等待數秒乃至數分鐘的時間才會發現自己剛才拼錯了一個單詞。由於瀏覽器只是一個純粹的檢視程式,所以連最簡單的計算任務都不能進行(當然在另一方面,它也顯得非常安全,因為不能在本機上面執行任何程式,避開了程式錯誤或者病毒的騷擾)。
    為解決這個問題,人們採取了許多不同的方法。最開始的時候,人們對圖形標準進行了改進,使瀏覽器能顯示更好的動畫和視訊。為解決剩下的問題,唯一的辦法就是在客戶端(瀏覽器)內執行程式。這就叫作“客戶端程式設計”,它是對傳統的“伺服器端程式設計”的一個非常重要的拓展。

1.11.2 客戶端程式設計(註釋⑧)
Web最初採用的“伺服器-瀏覽器”方案可提供互動式內容,但這種互動能力完全由伺服器提供,為伺服器和因特網帶來了不小的負擔。伺服器一般為客戶瀏覽器產生靜態網頁,由後者簡單地解釋並顯示出來。基本HTML語言提供了簡單的資料收集機制:文字輸入框、複選框、單選鈕、列表以及下拉列表等,另外還有一個按鈕,只能由程式規定重新設定表單中的資料,以便回傳給伺服器。使用者提交的資訊通過所有Web伺服器均能支援的“通用閘道器介面”(CGI)回傳到伺服器。包含在提交資料中的文字指示CGI該如何操作。最常見的行動是執行位於伺服器的一個程式。那個程式一般儲存在一個名為“cgi-bin”的目錄中(按下Web頁內的一個按鈕時,請注意一下瀏覽器頂部的地址窗,經常都能發現“cgi-bin”的字樣)。大多數語言都可用來編制這些程式,但其中最常見的是Perl。這是由於Perl是專為文字的處理及解釋而設計的,所以能在任何伺服器上安裝和使用,無論採用的處理器或作業系統是什麼。

⑧:本節內容改編自某位作者的一篇文章。那篇文章最早出現在位於www.mainspring.com的Mainspring上。本節的採用已徵得了對方的同意。
今天的許多Web站點都嚴格地建立在CGI的基礎上,事實上幾乎所有事情都可用CGI做到。唯一的問題就是響應時間。CGI程式的響應取決於需要傳送多少資料,以及伺服器和因特網兩方面的負擔有多重(而且CGI程式的啟動比較慢)。Web的早期設計者並未預料到當初綽綽有餘的頻寬很快就變得不夠用,這正是大量應用充斥網上造成的結果。例如,此時任何形式的動態圖形顯示都幾乎不能連貫地顯示,因為此時必須建立一個GIF檔案,再將圖形的每種變化從伺服器傳遞給客戶。而且大家應該對輸入表單上的資料校驗有著深刻的體會。原來的方法是我們按下網頁上的提交按鈕(Submit);資料回傳給伺服器;伺服器啟動一個CGI程式,檢查使用者輸入是否有錯;格式化一個HTML頁,通知可能遇到的錯誤,並將這個頁回傳給我們;隨後必須回到原先那個表單頁,再輸入一遍。這種方法不僅速度非常慢,也顯得非常繁瑣。
解決的辦法就是客戶端的程式設計。執行Web瀏覽器的大多數機器都擁有足夠強的能力,可進行其他大量工作。與此同時,原始的靜態HTML方法仍然可以採用,它會一直等到伺服器送回下一個頁。客戶端程式設計意味著Web瀏覽器可獲得更充分的利用,並可有效改善Web伺服器的互動(互動)能力。
對客戶端程式設計的討論與常規程式設計問題的討論並沒有太大的區別。採用的引數肯定是相同的,只是執行的平臺不同:Web瀏覽器就象一個有限的作業系統。無論如何,我們仍然需要程式設計,仍然會在客戶端程式設計中遇到大量問題,同時也有很多解決的方案。在本節剩下的部分裡,我們將對這些問題進行一番概括,並介紹在客戶端程式設計中採取的對策。

感謝Bruce Eckel 。