Java編程思想(第四版)繼承

來源:互聯網
上載者:User
文章目錄
  • 1.7   伴隨多態的可互換對象
  • 1.8   單根繼承結構

對象這種觀念,本身就是十分方便的工具,使得你可以通過概念將資料和功能封裝到一起,因此可以對問題空間的觀念給出恰當的表示,而不用受制於必須使用底層機器語言。這些概念用關鍵字class來表示,它們形成了程式設計語言中的基本單位。

遺憾的是,這樣做還是有很多麻煩:在建立了一個類之後,即使另一個新類與其具有相似的功能,你還是得重新建立一個新類。如果我們能夠以現有的類為基礎,複製它,然後通過添加和修改這個副本來建立新類那就要好多了。通過繼承便可以達到這樣的效果,不過也有例外,當源類(被稱為基類、超類或父類)發生變動時,被修改的“副本”(被稱為匯出類、繼承類或子類)也會反映出這些變動()。

(這張UML圖中的箭頭從匯出類指向基類,就像稍後你會看到的,通常會存在一個以上的匯出類。)

類型不僅僅只是描述了作用於一個對象集合上的約束條件,同時還有與其他類型之間的關係。兩個類型可以有相同的特性和行為,但是其中一個類型可能比另一個含有更多的特性,並且可以處理更多的訊息(或以不同的方式來處理訊息)。繼承使用基底類型和匯出類型的概念表示了這種類型之間的相似性。一個基底類型包含其所有匯出類型所共用的特性和行為。可以建立一個基底類型來表示系統中某些對象的核心概念,從基底類型中匯出其他類型,來表示此核心可以被實現的各種不同方式。

以記憶體回收機為例,它用來歸類散落的垃圾。“垃圾”是基底類型,每一件垃圾都有重量、價值等特性,可以被切碎、熔化或分解。在此基礎上,可以通過添加額外的特性(例如瓶子有顏色)或行為(例如鋁罐可以被壓碎,鐵罐可以被磁化)匯出更具體的垃圾類型。此外,某些行為可能不同(例如紙的價值取決於其類型和狀態)。可以通過使用繼承來構建一個類型階層,以此來表示待求解的某種類型的問題。

第二個例子是經典的幾何形的例子,這在電腦輔助設計系統或遊戲模擬系統中可能被用到。基類是幾何形,每一個幾何形都具有尺寸、顏色、位置等,同時每一個幾何形都可以被繪製、擦除、移動和著色等。在此基礎上,可以匯出(繼承出)具體的幾何形狀—圓形、正方形、三角形等—每一種都具有額外的特性和行為,例如某些形狀可以被翻轉。某些行為可能並不相同,例如計算幾何形狀的面積。類型階層同時體現了幾何形狀之間的相似性和差異性()。

以同樣的術語將解決方案轉換成問題是大有裨益的,因為不需要在問題描述和解決方案描述之間建立許多中間模型。通過使用對象,類型階層成為了主要模型,因此,可以直接從真實世界中對系統的描述過渡到用代碼對系統進行描述。事實上,對使用物件導向設計的人們來說,困難之一是從開始到結束過於簡單。對於訓練有素、善於尋找複雜的解決方案的頭腦來說,可能會在一開始被這種簡單性給難倒。

當繼承現有類型時,也就創造了新的類型。這個新的類型不僅包括現有類型的所有成員(儘管private成員被隱藏了起來,並且不可訪問),而且更重要的是它複製了基類的介面。也就是說,所有可以發送給基類對象的訊息同時也可以發送給匯出類對象。由於通過發送給類的訊息的類型可知類的類型,所以這也就意味著匯出類與基類具有相同的類型。在前面的例子中,“一個圓形也就是一個幾何形”。通過繼承而產生的類型等價性是理解物件導向程式設計方法內涵的重要門檻。

由於基類和匯出類具有相同的基礎介面,所以伴隨此介面的必定有某些具體實現。也就是說,當對象接收到特定訊息時,必須有某些代碼去執行。如果只是簡單地繼承一個類而並不做其他任何事,那麼在基類介面中的方法將會直接繼承到匯出類中。這意味著匯出類的對象不僅與基類擁有相同的類型,而且還擁有相同的行為,這樣做沒有什麼特別意義。

有兩種方法可以使基類與匯出類產生差異。第一種方法非常直接:直接在匯出類中添加新方法。這些新方法並不是基類介面的一部分。這意味著基類不能直接滿足你的所有需求,因此必需添加更多的方法。這種對繼承簡單而基本的使用方式,有時對問題來說確實是一種完美的解決方式。但是,應該仔細考慮是否存在基類也需要這些額外方法的可能性。這種設計的發現與迭代過程在物件導向程式設計中會經常發生()。

雖然繼承有時可能意味著在介面中添加新方法(尤其是在以extends關鍵字表示繼承的Java中),但並非總需如此。第二種也是更重要的一種使匯出類和基類之間產生差異的方法是改變現有基類的方法的行為,這被稱之為覆蓋(overriding)那個方法()。

要想覆蓋某個方法,可以直接在匯出類中建立該方法的新定義即可。你可以說:“此時,我正在使用相同的介面方法,但是我想在新類型中做些不同的事情。”

1.6.1   “是一個”與“像是一個”關係

對於繼承可能會引發某種爭論:繼承應該只覆蓋基類的方法(而並不添加在基類中沒有的新方法)嗎?如果這樣做,就意味著匯出類和基類是完全相同的類型,因為它們具有完全相同的介面。結果可以用一個匯出類對象來完全替代一個基類對象。這可以被視為純粹替代,通常稱之為替代原則。在某種意義上,這是一種處理繼承的理想方式。我們經常將這種情況下的基類與匯出類之間的關係稱為is-a(是一個)關係,因為可以說“一個圓形就是一個幾何形狀”。判斷是否繼承,就是要確定是否可以用is-a來描述類之間的關係,並使之具有實際意義。

有時必須在匯出類型中添加新的介面元素,這樣也就擴充了介面。這個新的類型仍然可以替代基類,但是這種替代並不完美,因為基類無法訪問新添加的方法。這種情況我們可以描述為is-like-a(像是一個)關係。新類型具有舊類型的介面,但是它還包含其他方法,所以不能說它們完全相同。以空調為例,假設房子裡已經布線安裝好了所有的冷氣裝置的控制器,也就是說,房子具備了讓你控製冷氣裝置的介面。想像一下,如果空調壞了,你用一個既能製冷又能制熱的熱力泵替換了它,那麼這個熱力泵就is-like-a空調,但是它可以做更多的事。因為房子的控制系統被設計為只能控製冷氣裝置,所以它只能和新對象中的製冷部分進行通訊。儘管新對象的介面已經被擴充了,但是現有系統除了原來介面之外,對其他東西一無所知。

當然,在看過這個設計之後,很顯然會發現,製冷系統這個基類不夠一般化,應該將其更名為“溫度控制系統”,使其可以包括制熱功能,這樣我們就可以套用替代原則了。這張圖說明了在真實世界中進行設計時可能會發生的事情。

當你看到替代原則時,很容易會認為這種方式(純粹替代)是唯一可行的方式,而且事實上,用這種方式設計是很好的。但是你會時常發現,同樣顯然的是你必須在匯出類的介面中添加新方法。只要仔細審視,兩種方法的使用場合應該是相當明顯的。

1.7   伴隨多態的可互換對象

在處理類型的階層時,經常想把一個對象不當作它所屬的特定類型來對待,而是將其當作其基類的對象來對待。這使得人們可以編寫出不依賴於特定類型的代碼。在“幾何形”的例子中,方法操作的都是泛化(generic)的形狀,而不關心它們是圓形、正方形、三角形還是其他什麼尚未定義的形狀。所有的幾何形狀都可以被繪製、擦除和移動,所以這些方法都是直接對一個幾何形對象發送訊息;它們不用擔心對象將如何處理訊息。

這樣的代碼是不會受添加新類型影響的,而且添加新類型是擴充一個物件導向程式以便處理新情況的最常用方式。例如,可以從“幾何形”中匯出一個新的子類型“五角形”,而並不需要修改處理泛化幾何形狀的方法。通過匯出新的子類型而輕鬆擴充設計的能力是對改動進行封裝的基本方式之一。這種能力可以極大地改善我們的設計,同時也降低軟體維護的代價。

但是,在試圖將匯出類型的對象當作其泛化基底類型對象來看待時(把圓形看作是幾何形,把單車看作是交通工具,把鸕鶿看作是鳥等等),仍然存在一個問題。如果某個方法要讓泛化幾何形狀繪製自己、讓泛化交通工具行駛,或者讓泛化的鳥類移動,那麼編譯器在編譯時間是不可能知道應該執行哪一段代碼的。這就是關鍵所在:當發送這樣的訊息時,程式員並不想知道哪一段代碼將被執行;繪圖方法可以被等同地應用於圓形、正方形、三角形,而對象會依據自身的具體類型來執行恰當的代碼。

如果不需要知道哪一段代碼會被執行,那麼當添加新的子類型時,不需要更改調用它的方法,它就能夠執行不同的代碼。因此,編譯器無法精確地瞭解哪一段代碼將會被執行,那麼它該怎麼辦呢?例如,在下面的圖中,BirdController對象僅僅處理泛化的Bird對象,而不瞭解它們的確切類型。從BirdController的角度看,這麼做非常方便,因為不需要編寫特別的代碼來判定要處理的Bird對象的確切類型或其行為。當move()方法被調用時,即便忽略Bird的具體類型,也會產生正確的行為(Goose(鵝)走、飛或遊泳,Penguin(企鵝)走或遊泳),那麼,這是如何發生的呢?

這個問題的答案,也是物件導向程式設計的最重要的妙訣:編譯器不可能產生傳統意義上的函數調用。一個非物件導向編程的編譯器產生的函數調用會引起所謂的前期綁定,這個術語你可能以前從未聽說過,可能從未想過函數調用的其他方式。這麼做意味著編譯器將產生對一個具體函數名字的調用,而運行時將這個調用解析到將要被執行的代碼的絕對位址。然而在OOP中,程式直到運行時才能夠確定代碼的地址,所以當訊息發送到一個泛化對象時,必須採用其他的機制。

為瞭解決這個問題,物件導向程式設計語言使用了後期綁定的概念。當向對象發送訊息時,被調用的代碼直到運行時才能確定。編譯器確保被呼叫者法的存在,並對調用參數和傳回值執行類型檢查(無法提供此類保證的語言被稱為是弱類型的),但是並不知道將被執行的確切代碼。

為了執行後期綁定,Java使用一小段特殊的代碼來替代絕對位址調用。這段代碼使用在對象中儲存的資訊來計算方法體的地址(這個過程將在第8章中詳述)。這樣,根據這一小段代碼的內容,每一個對象都可以具有不同的行為表現。當向一個對象發送訊息時,該對象就能夠知道對這條訊息應該做些什麼。

在某些語言中,必須明確地聲明希望某個方法具備後期綁定屬性所帶來的靈活性(C++是使用virtual關鍵字來實現的)。在這些語言中,方法在預設情況下不是動態綁定的。而在Java中,動態綁定是預設行為,不需要添加額外的關鍵字來實現多態。

再來看看幾何形狀的例子。整個類族(其中所有的類都基於相同的一致介面)在本章前面已有圖示。為了說明多態,我們要編寫一段代碼,它忽略類型的具體細節,僅僅和基類互動。這段代碼和具體類型資訊是分離的(decoupled),這樣做使代碼編寫更為簡單,也更易於理解。而且,如果通過繼承機制添加一個新類型,例如Hexagon(六邊形),所編寫的代碼對Shape(幾何形)的新類型的處理與對已有類型的處理會同樣出色。正因為如此,可以稱這個程式是可擴充的。

如果用Java來編寫一個方法(後面很快你就會學習如何編寫):

這個方法可以與任何Shape對話,因此它是獨立於任何它要繪製和擦除的對象的具體類型的。如果程式中其他部分用到了doSomething()方法:

對doSomething()的調用會自動地正確處理,而不管對象的確切類型。

這是一個相當令人驚奇的訣竅。看看下面這行代碼:

當Circle被傳入到預期接收Shape的方法中,究竟會發生什麼。由於Circle可以被doSomething()看作是Shape,也就是說,doSomething()可以發送給Shape的任何訊息,Circle都可以接收,那麼,這麼做是完全安全且合乎邏輯的。

把將匯出類看做是它的基類的過程稱為向上轉型(upcasting)。轉型(cast)這個名稱的靈感來自於模型鑄造的塑模動作;而向上(up)這個詞來源於繼承圖的典型布局方式:通常基類在頂部,而匯出類在其下部散開。因此,轉型為一個基類就是在繼承圖中向上移動,即“向上轉型”()。

一個物件導向程式肯定會在某處包含向上轉型,因為這正是將自己從必須知道確切類型中解放出來的關鍵。讓我們再看看doSomething()中的代碼:

注意這些代碼並不是說“如果是Circle,請這樣做;如果是Square,請那樣做……”。如果編寫了那種檢查Shape所有實際可能類型的代碼,那麼這段代碼肯定是雜亂不堪的,而且在每次添加了Shape的新類型之後都要去修改這段代碼。這裡所要表達的意思僅僅是“你是一個Shape,我知道你可以erase()和draw()你自己,那麼去做吧,但是要注意細節的正確性。”

doSomething()的代碼給人印象深刻之處在於,不知何故,它總是做了該做的。調用Circle的draw()方法所執行的代碼與調用Square或Line的draw()方法所執行的代碼是不同的,而且當draw()訊息被發送給一個匿名的Shape時,也會基於該Shape的實際類型產生正確的行為。這相當神奇,因為就像在前面提到的,當Java編譯器在編譯doSomething()的代碼時,並不能確切知道doSomething()要處理的確切類型。所以通常會期望它的編譯結果是調用基類Shape的erase()和draw()版本,而不是具體的Circle、Square或Line的相應版本。正是因為多態才使得事情總是能夠被正確處理。編譯器和運行系統會處理相關的細節,你需要馬上知道的只是事情會發生,更重要的是怎樣通過它來設計。當向一個對象發送訊息時,即使涉及向上轉型,該對象也知道要執行什麼樣的正確行為。

1.8   單根繼承結構

在OOP中,自C++面世以來就已變得非常矚目的一個問題就是,是否所有的類最終都繼承自單一的基類。在Java中(事實上還包括除C++以外的所有OOP語言),答案是yes,這個終極基類的名字就是Object。事實證明,單根繼承結構帶來了很多好處。

在單根繼承結構中的所有對象都具有一個共用介面,所以它們歸根到底都是相同的基本類型。另一種(C++所提供的)結構是無法確保所有對象都屬於同一個基本類型。從向後相容的角度看,這麼做能夠更好地適應C模型,而且受限較少,但是當要進行完全的物件導向程式設計時,則必須構建自己的繼承體系,使得它可以提供其他OOP語言內建的便利。並且在所獲得的任何新類庫中,總會用到一些不相容的介面,需要花力氣(有可能要通過多重繼承)來使新介面融入你的設計之中。這麼做來換取C++額外的靈活性是否值得呢?如果需要的話—如果在C上面投資巨大,這麼做就很有價值。如果是剛剛從頭開始,那麼像Java這樣的選擇通常會有更高的生產率。

單根繼承結構保證所有對象都具備某些功能。因此你知道,在你的系統中你可以在每個對象上執行某些基本操作。所有對象都可以很容易地在堆上建立,而參數傳遞也得到了極大的簡化。

單根繼承結構使記憶體回收行程的實現變得容易得多,而記憶體回收行程正是Java相對C++的重要改進之一。由於所有對象都保證具有其類型資訊,因此不會因無法確定對象的類型而陷入僵局。這對於系統級操作(如異常處理)顯得尤其重要,並且給編程帶來了更大的靈活性。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.