1. 程式人生 > >《Java程式設計思想》《Think in Java》筆記

《Java程式設計思想》《Think in Java》筆記

前言

這本書不適合初學者,這本書適合已經學過Java框架並做過一兩個專案的同學來看,這本書對基礎知識的理解非常透徹。我在看的時候常常有一種醍醐灌頂的感覺,常常為“原來是這樣子的!”而激動,確實是一本非常好的書。我在看書時將書本我覺得重點的內容原封不動的摘錄下來,給想看重點的同學看看,也為一些對某些基礎概念不是很懂的同學給個瞭解渠道。

第1章     物件入門

1.1   抽象的進步

       所有程式語言的最終目的都是提供一種“抽象”方法。一種較有爭議的說法是:解決問題的複雜程度直接取決於抽象的種類及質量。

       OOP允許我們根據問題來描述問題,而不是根據方案。

       “純粹”的面向物件程式設計方法:

       (1)所有東西都是物件。可將物件想象成一種新型變數;它儲存著資料,但可要求它對自身進行操作。理論上講,可從要解決的問題身上提出所有概念性的元件,然後在程式中將其表達為一個物件。

       (2)程式是一大堆物件的組合;通過訊息傳遞,各物件知道自己該做些什麼。為了向物件發出請求,需向那個物件“傳送一條訊息”。更具體地講,可將訊息想象為一個呼叫請求,它呼叫的是從屬於目標物件的一個子例程或函式。

       (3)每個物件都有自己的儲存空間,可容納其他物件。或者說,通過封裝現有物件,可製作出新型物件。所以,儘管物件的概念非常簡單,但在程式中卻可達到任意高的複雜程度。

       (4)每個物件都有一種型別

。根據語法,每個物件都是某個“類”的一個“例項”。其中,“類”(Class)是“型別”(Type)的同義詞。一個類最重要的特徵就是“能將什麼訊息發給它?”。

       (5)同一類所有物件都能接收相同的訊息。這實際是別有含義的一種說法,大家不久便能理解。由於型別為“圓”(Circle)的一個物件也屬於型別為“形狀”(Shape)的一個物件,所以一個圓完全能接收形狀訊息。這意味著可讓程式程式碼統一指揮“形狀”,令其自動控制所有符合“形狀”描述的物件,其中自然包括“圓”。這一特性稱為物件的“可替換性”,OOP[莫星燦1] 最重要的概念之一

1.2 物件的介面

       當我們進行面向物件的程式設計時,面臨的最大一項挑戰性就是:如何在“問題空間”(問題實際存在的地方)的元素與“方案空間”(對實際問題進行建模的地方,如計算機)的元素之間建立理想的“一對一”對應或對映關係。

       如何利用物件完成真正有用的工作呢?必須有一種辦法能向物件發出請求,令其做一些實際的事情,比如完成一次交易、在螢幕上畫一些東西或者開啟一個開關等等。每個物件僅能接受特定的請求。我們向物件發出的請求是通過它的“介面”(Interface)定義的,物件的“型別”或“類”則規定了它的介面形式。“型別”與“介面”的等價或對應關係是面向物件程式設計的基礎。

       以電燈泡為例:

       在這個例子中,型別/類的名稱是 Light,可向 Light 物件發出的請求包括包括開啟(on)、關閉(off)、變得更明亮(brighten)或者變得更暗淡(dim)。通過簡單地宣告一個名字(lt),我們為 Light 物件建立了一個控制代碼[莫星燦2] 。然後用new關鍵字新建型別為 Light 的一個物件。再用等號將其賦給控制代碼。為了向物件傳送一條訊息,我們列出控制代碼名(lt),再用一個句點符號(.)把它同訊息名稱(on)連線起來。[莫星燦3] 從中可以看出,使用一些預先定義好的類時,我們在程式裡採用的程式碼是非常簡單和直觀的。

1.3 實現方案的隱藏

       從根本上說,大致有兩方面的人員涉足面向物件的程式設計:“類建立者”(建立新資料型別的人)以及客戶程式設計師[莫星燦4] (在自己的應用程式中採用現成資料型別的人)。

       “介面”Interface)規定了可對一個特定的物件發出哪些請求。然而,必須在某個地方存在著一些程式碼,以便滿足這些請求。這些程式碼與那些隱藏起來的資料便叫作“隱藏的實現”。站在程式化程式編寫(Procedural Programming)的角度,整個問題並不顯得複雜。一種型別含有與每種可能的請求關聯起來的函式。一旦向物件發出一個特定的請求,就會呼叫那個函式。我們通常將這個過程總結為向物件“傳送一條訊息”(提出一個請求)。物件的職責就是決定如何對這條訊息作出反應(執行相應的程式碼)。

       Java採用三個顯式(明確)關鍵字以及一個隱式(暗示)關鍵字來設定類邊界public,private,protected 以及暗示性的friendly[莫星燦5] 。若未明確指定其他關鍵字,則預設為後者。這些關鍵字的使用和含義都是相當直觀的,它們決定了誰能使用後續的定義內容。“public”(公共)意味著後續的定義任何人均可使用。而在另一方面,“private”(私有)意味著除您自己、型別的建立者以及那個型別的內部函式成員,其他任何人都不能訪問後續的定義資訊。private在您與客戶程式設計師之間豎起了一堵牆。若有人試圖訪問私有成員,就會得到一個編譯期錯誤。“friendly”(友好的)涉及“包裝”或“封裝”(Package)的概念——即Java 用來構建庫的方法。若某樣東西是“友好的”,意味著它只能在這個包裝的範圍內使用(所以這一訪問級別有時也叫作“包裝訪問”)。“protected”(受保護的)與“private”相似,只是一個繼承的類可訪問受保護的成員,但不能訪問私有成員。繼承的問題不久就要談到。

1.4 方案的重複使用

       為重複使用一個類,最簡單的辦法是僅直接使用那個類的物件。但同時也能將那個類的一個物件置入一個新類。我們把這叫作“建立一個成員物件”。新類可由任意數量和型別的其他物件構成。無論如何,只要新類達到了設計要求即可。這個概念叫作“組織”——在現有類的基礎上組織一個新類。有時,我們也將組織稱作“包含”關係,比如“一輛車包含了一個變速箱”。

[莫星燦6] 1.5 繼承:重新使用介面

       使用繼承時,相當於建立了一個新類。這個新類不僅包含了現有型別的所有成員(儘管private 成員被隱藏起來,且不能訪問),但更重要的是,它複製了基礎類的介面。也就是說,可向基礎類的物件傳送的所有訊息亦可原樣發給衍生類的物件。根據可以傳送的訊息,我們能知道類的型別。這意味著衍生類具有與基礎類相同的型別!為真正理解面向物件程式設計的含義,首先必須認識到這種型別的等價關係。

1.5.1 改善基礎類

       為改善一個函式,只需為衍生類的函式建立一個新定義即可。我們的目標是:“儘管使用的函式介面未變,但它的新版本具有不同的表現”。

1.5.2 等價與類似關係

       但在許多時候,我們必須為衍生型別加入新的介面元素。所以不僅擴充套件了介面,也建立了一種新型別。這種新型別仍可替換成基礎型別,但這種替換並不是完美的,因為不可在基礎類裡訪問新函式。我們將其稱作“類似”關係;新型別擁有舊型別的介面,但也包含了其他函式,所以不能說它們是完全等價的。

1.6 多形物件的互換使用

       通常,繼承最終會以建立一系列類收場,所有類都建立在統一的介面基礎上。我們用一幅顛倒的樹形圖來闡明這一點(註釋⑤):

⑤:這兒採用了“統一記號法”,本書將主要採用這種方法。

       對這樣的一系列類,我們要進行的一項重要處理就是將衍生類的物件當作基礎類的一個物件對待。這一點是非常重要的,因為它意味著我們只需編寫單一的程式碼,令其忽略型別的特定細節,只與基礎類打交道。這樣一來,那些程式碼就可與型別資訊分開。所以更易編寫,也更易理解。此外,若通過繼承增添了一種新型別,如“三角形”,那麼我們為“幾何形狀”新型別編寫的程式碼會象在舊型別裡一樣良好地工作。所以說程式具備了“擴充套件能力”,具有“擴充套件性”。

       以上面的例子為基礎,假設我們用 Java 寫了這樣一個函式:

Void doStuff(Shape s){

  s.erase();

  //…

  s.draw();

}

       這個函式可與任何“幾何形狀”(Shape)通訊,所以完全獨立於它要描繪(draw)和刪除(erase)的任何特定型別的物件。如果我們在其他一些程式裡使用 doStuff()函式:

       那麼對doStuff()的呼叫會自動良好地工作,無論物件的具體型別是什麼。

       這實際是一個非常有用的程式設計技巧。請考慮下面這行程式碼:

       doStuff(c);

       此時,一個 Circle(圓)控制代碼傳遞給一個本來期待 Shape(形狀)控制代碼的函式。由於圓是一種幾何形狀,所以doStuff()能正確地進行處理。也就是說,凡是 doStuff()能發給一個 Shape的訊息,Circle也能接收。所以這樣做是安全的,不會造成錯誤。

       我們將這種把衍生型別當作它的基本型別處理的過程叫作“Upcasting”(上溯造型)。其中,“cast”(造型)是指根據一個現成的模型建立;而“Up”(向上)表明繼承的方向是從“上面”來的——即基礎類位於頂部,而衍生類在下方展開。所以,根據基礎類進行造型就是一個從上面繼承的過程,即“Upcasting”。在面向物件的程式裡,通常都要用到上溯造型技術。這是避免去調查準確型別的一個好辦法。請看看doStuff()裡的程式碼:

s.erase();

// ...

s.draw();

       注意它並未這樣表達:“如果你是一個Circle,就這樣做;如果你是一個Square,就那樣做;等等”。若那樣編寫程式碼,就需檢查一個Shape 所有可能的型別,如圓、矩形等等。這顯然是非常麻煩的,而且每次添加了一種新的 Shape型別後,都要相應地進行修改。在這兒,我們只需說:“你是一種幾何形狀,我知道你能將自己刪掉,即erase();請自己採取那個行動,並自己去控制所有的細節吧。”

1.6.1 動態繫結

       將一條訊息發給物件時,如果並不知道對方的具體型別是什麼,但採取的行動同樣是正確的,這種情況就叫作“多形性”(Polymorphism)。對面向物件的程式設計語言來說,它們用以實現多形性的方法叫作“動態繫結[莫星燦8] 。編譯器和執行期系統會負責對所有細節的控制;我們只需知道會發生什麼事情,而且更重要的是,如何利用它幫助自己設計程式。

1.6.2 抽象的基礎類和介面

       亦可用abstract 關鍵字描述一個尚未實現的方法——作為一個“”使用,指出:“這是適用於從這個類繼承的所有型別的一個介面函式,但目前尚沒有對它進行任何形式的實現。”抽象方法也許只能在一個抽象類裡建立。繼承了一個類後,那個方法就必須實現,否則繼承的類也會變成“抽象”類。通過建立一個抽象方法,我們可以將一個方法置入介面中,不必再為那個方法提供可能毫無意義的主體程式碼。

       interface(介面)關鍵字將抽象類的概念更延伸了一步,它完全禁止了所有的函式定義。“介面”是一種相當有效和常用的工具。另外如果自己願意,亦可將多個介面都合併到一起(不能從多個普通class 或abstract class 中繼承)。

1.7 物件的建立和存在時間

從技術角度說,OOP(面向物件程式設計)只是涉及抽象的資料型別、繼承以及多形性,但另一些問題也可能顯得非常重要。本節將就這些問題進行探討。

最重要的問題之一是物件的建立及破壞方式。物件需要的資料位於哪兒,如何控制物件的“存在時間”呢?針對這個問題,解決的方案是各異其趣的。

第二個方法是在一個記憶體池中動態建立物件,該記憶體池亦叫“”或者“記憶體堆”。若採用這種方式,除非進入執行期,否則根本不知道到底需要多少個物件,也不知道它們的存在時間有多長,以及準確的型別是什麼。這些引數都在程式正式執行時才決定的。若需一個新物件,只需在需要它的時候在記憶體堆裡簡單地建立它即可。由於儲存空間的管理是執行期間動態進行的,所以在記憶體堆裡分配儲存空間的時間比在堆疊裡建立的時間長得多(在堆疊裡建立儲存空間一般只需要一個簡單的指令,將堆疊指標向下或向下移動即可)。由於動態建立方法使物件本來就傾向於複雜,所以查詢儲存空間以及釋放它所需的額外開銷不會為物件的建立造成明顯的影響。除此以外,更大的靈活性對於常規程式設計問題的解決是至關重要的。

1.7.1 集合與繼承器

針對一個特定問題的解決,如果事先不知道需要多少個物件,或者它們的持續時間有多長,那麼也不知道如何儲存那些物件。既然如此,怎樣才能知道那些物件要求多少空間呢?事先上根本無法提前知道,除非進入執行期。

繼續器”(Iterator),它屬於一種物件,負責選擇集合內的元素,並把它們提供給繼

承器的使用者。作為一個類,它也提供了一級抽象。利用這一級抽象,可將集合細節與用於訪問那個集合的程式碼隔離開。通過繼承器的作用,集合被抽象成一個簡單的序列。繼承器允許我們遍歷那個序列,同時毋需關心基礎結構是什麼——換言之,不管它是一個向量、一個連結列表、一個堆疊,還是其他什麼東西。這樣一來,我們就可以靈活地改變基礎資料,不會對程式裡的程式碼造成干擾。

1.7.2 單根結構

在面向物件的程式設計中,由於C++的引入而顯得尤為突出的一個問題是:所有類最終是否都應從單獨一個基礎類繼承。在Java 中(與其他幾乎所有OOP語言一樣),對這個問題的答案都是肯定的,而且這個終級基礎類的名字很簡單,就是一個“Object[莫星燦10] ”。

       單根結構中的所有物件都有一個通用介面,所以它們最終都屬於相同的型別。

       單根結構中的所有物件(比如所有 Java 物件)都可以保證擁有一些特定的功能。

       利用單根結構,我們可以更方便地實現一個垃圾收集器。與此有關的必要支援可安裝於基礎類中,而垃圾收集器可將適當的訊息發給系統內的任何物件。如果沒有這種單根結構,而且系統通過一個控制代碼來操縱物件,那麼實現垃圾收集器的途徑會有很大的不同,而且會面臨許多障礙。

1.7.3 集合庫與方便使用集合

由於集合是我們經常都要用到的一種工具,所以一個集合庫是十分必要的,它應該可以方便地重複使用。這樣一來,我們就可以方便地取用各種集合,將其插入自己的程式。Java 提供了這樣的一個庫,儘管它在Java1.0和 1.1中都顯得非常有限。

1. 下溯造型與模板/通用性

為了使這些集合能夠重複使用,或者“再生”,Java 提供了一種通用型別,以前曾把它叫作“Object”。單根結構意味著、所有東西歸根結底都是一個物件”!所以容納了Object 的一個集合實際可以容納任何東西。這使我們對它的重複使用變得非常簡便。

為使用這樣的一個集合,只需新增指向它的物件控制代碼即可,以後可以通過控制代碼重新使用物件。但由於集合只能容納Object,所以在我們向集合裡新增物件控制代碼時,它會上溯造型成 Object,這樣便丟失了它的身份或者標識資訊。再次使用它的時候,會得到一個Object 控制代碼,而非指向我們早先置入的那個型別的控制代碼。所以怎樣才能歸還它的本來面貌,呼叫早先置入集合的那個物件的有用介面呢?

在這裡,我們再次用到了造型(Cast)。但這一次不是在分級結構中上溯造型成一種更“通用”的型別。而是下溯造型成一種更“特殊”的型別。這種造型方法叫作“下溯造型[莫星燦11] ”(Downcasting)。舉個例子來說,我們知道在上溯造型的時候,Circle(圓)屬於Shape(幾何形狀)的一種型別,所以上溯造型是安全的。但我們不知道一個Object到底是 Circle 還是Shape,所以很難保證下溯造型的安全進行,除非確切地知道自己要操作的是什麼。

1.7.4 清除時的困境:由誰負責清除?

在Java 中,垃圾收集器在設計時已考慮到了記憶體的釋放問題(儘管這並不包括清除一個物件涉及到的其他方面)。垃圾收集器“知道”一個物件在什麼時候不再使用,然後會自動釋放那個物件佔據的記憶體空間。採用這種方式,另外加上所有物件都從單個根類Object 繼承的事實,而且由於我們只能在記憶體堆中以一種方式建立物件,所以Java 的程式設計要比 C++的程式設計簡單得多。我們只需要作出少量的抉擇,即可克服原先存在的大量障礙。

1.    垃圾收集器對效率及靈活性的影響

既然這是如此好的一種手段,為什麼在C++裡沒有得到充分的發揮呢?我們當然要為這種程式設計的方便性付出

一定的代價,代價就是執行期的開銷。正如早先提到的那樣,在C++中,我們可在堆疊中建立物件。在這種情況下,物件會得以自動清除(但不具有在執行期間隨心所欲建立物件的靈活性)。在堆疊中建立物件是為物件分配儲存空間最有效的一種方式,也是釋放那些空間最有效的一種方式。在記憶體堆(Heap)中建立物件可能要付出昂貴得多的代價。如果總是從同一個基礎類繼承,並使所有函式呼叫都具有“同質多形”特徵,那麼也不可避免地需要付出一定的代價。但垃圾收集器是一種特殊的問題,因為我們永遠不能確定它什麼時候啟動或者要花多長的時間。這意味著在Java 程式執行期間,存在著一種不連貫的因素。所以在某些特殊的場合,我們必須避免用它——比如在一個程式的執行必須保持穩定、連貫的時候(通常把它們叫作“實時程式”,儘管並不是所有實時程式設計問題都要這方面的要求)。

1.8 違例控制:解決錯誤

1.9 多執行緒

有些時候,中斷對那些實時性很強的任務來說是很有必要的。但還存在其他許多問題,它們只要求將問題劃分進入獨立執行的程式片斷中,使整個程式能更迅速地響應使用者的請求。在一個程式中,這些獨立執行的片斷叫作“執行緒”(Thread),利用它程式設計的概念就叫作“多執行緒處理”。多執行緒處理一個常見的例子就是使用者介面。利用執行緒,使用者可按下一個按鈕,然後程式會立即作出響應,而不是讓使用者等待程式完成了當前任務以後才開始響應。

最開始,執行緒只是用於分配單個處理器的處理時間的一種工具。但假如作業系統本身支援多個處理器,那麼每個執行緒都可分配給一個不同的處理器,真正進入“並行運算”狀態。從程式設計語言的角度看,多執行緒操作最有價值的特性之一就是程式設計師不必關心到底使用了多少個處理器。程式在邏輯意義上被分割為數個執行緒;假如機器本身安裝了多個處理器,那麼程式會執行得更快,毋需作出任何特殊的調校。根據前面的論述,大家可能感覺執行緒處理非常簡單。但必須注意一個問題:共享資源!如果有多個執行緒同時執行,而且它們試圖訪問相同的資源,就會遇到一個問題。舉個例子來說,兩個程序不能將資訊同時傳送給一臺印表機。為解決這個問題,對那些可共享的資源來說(比如印表機),它們在使用期間必須進入鎖定狀態。所以一個執行緒可將資源鎖定,在完成了它的任務後,再解開(釋放)這個,使其他執行緒可以接著使用同樣的資源。

Java 的多執行緒機制已內建到語言中,這使一個可能較複雜的問題變得簡單起來。對多執行緒處理的支援是在物件這一級支援的,所以一個執行執行緒可表達為一個物件。Java 也提供了有限的資源鎖定方案。它能鎖定任何物件佔用的記憶體(記憶體實際是多種共享資源的一種),所以同一時間只能有一個執行緒使用特定的記憶體空間。為達到這個目的,需要使用synchronized關鍵字。其他型別的資源必須由程式設計師明確鎖定,這通常要求程式設計師建立一個物件,用它代表一把鎖,所有執行緒在訪問那個資源時都必須檢查這把鎖

1.10 永久性

Java 1.1 提供了對“有限永久性”的支援,這意味著我們可將物件簡單地儲存到磁碟上,以後任何時間都可取回。之所以稱它為“有限”的,是由於我們仍然需要明確發出呼叫,進行物件的儲存和取回工作。這些工作不能自動進行。在Java 未來的版本中,對“永久性”的支援有望更加全面。

1.11 Java 和因特網

Java 除了可解決傳統的程式設計問題以外,還能解決World Wide Web(全球資訊網)上的程式設計問題。

1.11.1 什麼是 Web ?

1. 客戶機/伺服器計算

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

2. Web是一個巨大的伺服器

Web實際就是一套規模巨大的客戶機/伺服器系統。但它的情況要複雜一些,因為所有伺服器和客戶都同時存在於單個網路上面。但我們沒必要了解更進一步的細節,因為唯一要關心的就是一次建立同一個伺服器的連線,並同它打交道(即使可能要在全世界的範圍內搜尋正確的伺服器)。

Web瀏覽器的發展終於邁出了重要的一步:某個資訊可在任何型別的計算機上顯示出來,毋需任何改動。然而,瀏覽器仍然顯得很原始,在使用者迅速增多的要求面前顯得有些力不從心。它們的互動能力不夠強,而且對伺服器和因特網都造成了一定程度的干擾。這是由於每次採取一些要求程式設計的操作時,必須將資訊反饋回伺服器,在伺服器那一端進行處理。所以完全可能需要等待數秒乃至數分鐘的時間才會發現自己剛才拼錯了一個單詞。由於瀏覽器只是一個純粹的檢視程式,所以連最簡單的計算任務都不能進行(當然在另一方面,它也顯得非常安全,因為不能在本機上面執行任何程式,避開了程式錯誤或者病毒的騷擾)。

 [莫星燦1]OOP: 面向物件程式設計

 [莫星燦2]終於知道控制代碼是什麼鬼了!

 [莫星燦5]較形象地理解這四個關鍵字

 “什麼人說什麼話”道理類似

[莫星燦10]所有類都繼承Object

 [莫星燦12]Exception Try{ }catch{ }的理解

 [莫星燦13]形象生動的詮釋何為伺服器和客戶,BC端

 [莫星燦14]大公司如阿里這種應該對這個很有研究了