Java 基礎(一)重新理解物件導向

diamond_lin發表於2017-09-22

抽象的進步

組合語言對基礎機器的少量抽象;
命令式語言是對組合語言的一種抽象;

物件的五大基本特徵

  • 所有的東西都是物件
  • 程式是一大堆物件的組合
  • 每個物件都有自己的儲存空間,可容納其他物件
  • 每個物件都有一種型別,一個類最重要的特徵就是“能將生命訊息發給他”
  • 同一類所有物件都能接受相同的訊息

物件的介面

如何利用物件完成真正有用的工作呢?必須有一種辦法能向物件發出請求,令其做一些實際的事情。介面就是對一個物件的行為進行規範,使物件具有做某些事情的能力。

實現方案的隱藏

假如現在有一個這樣的需求:送外賣。對於老闆來說,最主要的目標就是把外賣送到客戶手上,而對於配送員來說,他要做的是領取外賣、規劃路線、送到買家手上,只向老闆提供送外賣的能力(介面),其他所有的細節都隱藏。為什麼要這樣做?隱藏之後,老闆就不能接觸和改變那些細節,所以配送員也不會擔心老闆會干擾他用什麼交通工具去配送,可確保不會影響外賣正常送到。

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

對於任何關係,重要的一點是讓牽連到的所有成員都遵守相同的規則。建立一個配送員時,相當於和老闆建立了一種關係,對方也是人類,但他們的目標是組合出一個特定的平臺,如“餓了麼”。

有兩個方面的原因促使我們控制對成員的訪問,第一個就是防止籃板干擾配送員送外賣的行為---通常是內部資料型別的設計思想。若只是為了解決特定的問題,老闆注意要操作“配送”介面即可。配送員向老闆提供的實際是一種配送服務,因為老闆只關心配送員能否幫他送外賣,而對配送員怎麼送外賣並不關心。

進行訪問控制的第二個原因是允許配送員修改內部結構,不用擔心會對老闆造成什麼影響。比如說配送員找了個女朋友。

若任何人都能使用一個類的所有成員,那麼老闆可以對配送員 A 操作任何事情,比如說不能騎車去送外賣,外賣送達要規則給客戶,配送員 B 怕配送員 A 業績超過自己,偷偷的把配送員 A 的小電驢扎爆胎,那麼整個平臺就亂套了。

Java 採用了三個顯式關鍵字以及一個隱式關鍵字來設定類邊界。public、private、protected 以及暗示性的friendly。關鍵字的邊界就不再贅述了,不懂的小夥伴自行搜尋。

方案的重複使用

建立並測試好一個類之後,從理想的角度來說它應代表一個有用的單位。但並不像許多人期望的那樣,這種重複使用的能力並不容易實現;它要求較多的經驗及洞察力,這樣才能設計出一個好的方案,才有可能重複使用

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

物件的組織具有極大的靈活性。新類的“成員物件”通常設為“私有”(rpivate),使其不能被其他類訪問。這樣一來,我們可以在不影響其他類的前提下,從容的修改那些成員。這進一步增大了靈活性。後面要講的“繼承”並不具有這種靈活性,因為編譯器必須對通過繼承建立的類加以限制以提高複用性。

可能上面這一段講得有點抽象,我們再拿剛剛那個配送員的女朋友來舉例:女朋友具有做飯的能力,通常女朋友都是私有的(private)只給自己做飯吃。但是如果女朋友變成公有的(public),配送員 B 也用 A 的女朋友做飯吃。此時問題就出現了,配送員 A 要是和換了個不會做飯的女朋友, B 就會沒飯吃。

繼承:重新使用介面

就其本身來說,物件的概念可為我們帶來極大的便利。它在概念上允許我們 將各式各樣資料和功能封裝到一起。這樣便可恰當表達“問題空間”的概念,不 用刻意遵照基礎機器的表達方式。在程式設計語言中,這些概念則反映為具體的 資料型別(使用 class 關鍵字)。

我們費盡心思做出一種資料型別後,假如不得不又新建一種型別,令其實現 大致相同的功能,那會是一件非常令人灰心的事情。但若能利用現成的資料型別, 對其進行“克隆”,再根據情況進行新增和修改,情況就顯得理想多了。“繼承” 正是針對這個目標而設計的。但繼承並不完全等價於克隆。在繼承過程中,若原 始類(正式名稱叫作基礎類、超類或父類)發生了變化,修改過的“克隆”類(正式名稱叫作繼承類或者子類)也會反映出這種變化。在 Java 語言中,繼承是通 過 extends 關鍵字實現的

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

還拿剛剛送外賣來說,配送員 A 騎自行車送外賣有點慢,為了更快的送達外賣,現在招聘了配送員的兒子配送員 AA(會騎摩托車),配送員 AA 繼承了 A 的送外賣能力,同樣可以送外賣。

由於基礎類和衍生類具有相同的介面,所以那個介面必須進行特殊的設計。 也就是說,物件接收到一條特定的訊息後,必須有一個“方法”能夠執行。若只 是簡單地繼承一個類,並不做其他任何事情,來自基礎類介面的方法就會直接照 搬到衍生類。這意味著衍生類的物件不僅有相同的型別,也有同樣的行為,這一 後果通常是我們不願見到的。

剛剛我們說了為了更快的送達外賣,所以才招聘A 的兒子 AA,但是如果 AA 也只從父親 A 那裡學會騎自行車送外賣,那就得不償失了,所以我們需要的 AA 必須是會騎摩托車的。

有兩種做法可將新得的衍生類與原來的基礎類區分開。第一種做法十分簡 單:為衍生類新增新函式(功能)。這些新函式並非基礎類介面的一部分。進行 這種處理時,一般都是意識到基礎類不能滿足我們的要求,所以需要新增更多的 函式。這是一種最簡單、最基本的繼承用法,大多數時候都可完美地解決我們的 問題。然而,事先還是要仔細調查自己的基礎類是否真的需要這些額外的函式。

等價與類似關係

針對繼承可能會產生這樣的一個爭論:繼承只能改善原基礎類的函式嗎?若 答案是肯定的,則衍生型別就是與基礎類完全相同的型別,因為都擁有完全相同的介面。這樣造成的結果就是:我們完全能夠將衍生類的一個物件換成基礎類的一個物件!可將其想象成一種“純替換”。在某種意義上,這是進行繼承的一種理想方式。此時,我們通常認為基礎類和衍生類之間存在一種“等價”關係——因為我們可以理直氣壯地說:“圓就是一種幾何形狀”。為了對繼承進行測試,一 個辦法就是看看自己是否能把它們套入這種“等價”關係中,看看是否有意義。

但在許多時候,我們必須為衍生型別加入新的介面元素。所以不僅擴充套件了接 口,也建立了一種新型別。這種新型別仍可替換成基礎型別,但這種替換並不是完美的,因為不可在基礎類裡訪問新函式。我們將其稱作“類似”關係;新型別擁有舊型別的介面,但也包含了其他函式,所以不能說它們是完全等價的。舉個例子來說,讓我們考慮一下製冷機的情況。假定我們的房間連好了用於製冷的各 種控制器;也就是說,我們已擁有必要的“介面”來控制製冷。現在假設機器出了故障,我們把它換成一臺新型的冷、熱兩用空調,冬天和夏天均可使用。冷、熱空調“類似”製冷機,但能做更多的事情。由於我們的房間只安裝了控制製冷 的裝置,所以它們只限於同新機器的製冷部分打交道。新機器的介面已得到了擴 展,但現有的系統並不知道除原始介面以外的任何東西。

認識了等價與類似的區別後,再進行替換時就會有把握得多。儘管大多數時 候“純替換”已經足夠,但您會發現在某些情況下,仍然有明顯的理由需要在衍生類的基礎上增添新功能。通過前面對這兩種情況的討論,相信大家已心中有數 該如何做。

多形物件的互換使用

通常,繼承最終會以建立一系列類收場,所有類都建立在統一的介面基礎上。比如說一家公司要正常執行run(),需要招三個幹活的人,而Worker 都具有幹活的能力,只是不同的人具備不同的技能而已。現在公司如果要執行的話,只需要呼叫所有的“Worker”的 doWork 方法即可。而並不用關心是程式設計師寫程式碼還是設計師做設計。

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

動態繫結

比如說,一個公司在執行的時候要控制三個員工工作。在公司正常執行 run()的過程中,最讓人吃驚的是儘管我們沒作出任何特殊指示,採取的操作也是完全正確和恰當的。我們知道,為 程式設計師 呼叫 doWork()時執行的程式碼與 為一個 設計師 或 產品 呼叫 doWork()時執行的程式碼是不同的。但在將doWork()訊息發 給一個匿名 Worker 時,根據 Worker控制程式碼當時連線的實際型別,會相應地採取正確 的操作。這當然令人驚訝,因為當 Java 編譯器為 公司正常執行為run()編譯程式碼時,它並不 知道自己要操作的準確型別是什麼。儘管我們確實可以保證最終會為 Worker 呼叫doWork(),但並不能保證為特定的 程式設計師,設計師 或者 產品 呼叫什麼。然而最後採取的操作同樣是正確的,這是怎麼做到的呢?

將一條訊息發給物件時,如果並不知道對方的具體型別是什麼,但採取的行 動同樣是正確的,這種情況就叫作“多形性”(Polymorphism)。對物件導向的程式設計語言來說,它們用以實現多形性的方法叫作“動態繫結”。編譯器和執行期系統會負責對所有細節的控制;我們只需知道會發生什麼事情,而且更重要的 是,如何利用它幫助自己設計程式。有些語言要求我們用一個特殊的關鍵字來允許動態繫結。在 C++中,這個關 鍵字是 virtual。在 Java中,我們則完全不必記住新增一個關鍵字,因為函式的 動態繫結是自動進行的。所以在將一條訊息發給物件時,我們完全可以肯定物件會採取正確的行動,即使其中涉及上溯造型之類的處理。

----讀《Thinking in Java》有感

相關文章