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 。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章