對象導論學習筆記

」咱們之因此將天然界分解,組織成各類概念,並按其含義分類,主要是由於咱們是整個口語交流社會共同遵照的協定的參與者,這個協定以語言的形式固定下來······除非同意這個協定中規定的有關語言信息的組織和分類,不然咱們根本沒法交談。「java

 

本章將向讀者介紹包括開發方法概述在內的OOP的基本概念。程序員

 

1.1 抽象過程算法

     全部編程語言都提供抽象機制。能夠認爲,人們所可以解決的問題的複雜性直接取決於抽象的類型和質量。編程

     所謂的」類型「是指」所要抽象的是什麼?「數組

 

彙編語言是對底層機器的輕微抽象。接着出現的許多所謂」命令式「語言(如FORTRAN、BASIC、C等)都是對彙編語言的抽象。這些語言在彙編語言基礎上有了大幅的改進,可是它們所作的主要抽象仍要求在解決問題時要基於計算機的結構,而不是基於所要解決的問題的結構來考慮。程序員必須創建起在機器模型(位於」解空間「內,這是你對問題建模的地方,例如計算機)和實際待解問題的模型(位於」問題空間「內,這是問題存在的地方,例如一項業務)之間的關聯。創建這種映射是費力的,並且這不屬於編程語言固有的功能,這使得程序難以編寫,而且維護代價高昂,同事也產生了做爲副產物的整個」編程方法「行業。安全

 

另外一種對機器建模的方式就是隻針對待解問題建模。早期的編程語言,如LISP和APL,都是 選擇考慮世界的某些特定視圖(分別對應於「全部問題最終都是列表」或者"全部問題都是算法形式的」)。PROLOG則將全部問題都轉化成決策鏈。此外還產生了基於約束條件編程的語言和專門經過對圖形符號操做來實現編程的語言(後者被證實限制性太強)。編程語言

 

面向對象方式經過向程序員提供表示問題空間中的元素的工具而更進了一步。這種表示方法很是通用,使得程序員不會受限於任何特定類型的問題。咱們將問題空間中的元素及其在解空間中表示稱爲」對象「。(你還須要一些沒法類比爲問題空間元素的對象。)這種思想的實質是:程序能夠經過添加新類型的對象使自身使用於某個特定問題。所以,當你在閱讀描述解決方案的代碼的同時,也是在閱讀問題的表述。相比之前咱們所使用的語言,這是一種更靈活和更強有力的語言抽象。因此,OOP容許根據問題來描述問題,而不是根據運行解決方案的計算機來描述問題。可是它仍然與計算機有聯繫:每一個對象看起來都有點像一臺微型計算機------它具備狀態,還具備操做,用戶能夠要求對象執行這些操做。若是要對現實世界中的對象做類比,那麼說它們都具備特性和行爲彷佛不錯。函數

 

(我的理解)C語言就是編寫一堆無組織的操做(方法或者說函數);而java的改變就是用對象來組織C中的那些無序的方法。工具

 

純粹面向對象程序設計方式:佈局

1)萬物皆爲對象。

將對象視爲奇特的變量,它能夠存儲數據,除此以外,你還能夠要求它在自身上執行操做。理論上講你能夠抽取待求解問題的任何概念化構件(狗、建築物、服務等),將其表示爲程序中的對象。

2)程序視對象的集合,它們經過發送消息來告知彼此所要作的。要想請求一個對象,就必須對該對象發送一條消息。更具體地說,能夠把消息想象爲對某個特定對象的方法的調用請求。

3)每一個對象都有本身的由其餘對象所構成的存儲。換句話說,能夠經過建立包含現有對象的包的方式來建立新類型的對象。所以,能夠在程序中構建複雜的體系,同時將其複雜性隱藏在對象的簡單性背後。

4)每一個對象都擁有其類型。按照通用的說法,」每一個對象都是某個類(class)的一個實例(instance)「,整個」類「就是」類型「的同義詞。每一個類最重要的區別於其餘類的特性就是」能夠發送什麼樣的消息給它「。

5)某一特定類型的全部對象均可以接收一樣的消息。這是一句意味深長的表達,你在稍後便會看到。由於」圓形「類型的對象同時也是」幾何形「類型的對象,因此一個」圓形「對象一定可以接受發送給」幾何形「對象的信息。這意味着能夠編寫與」幾何形「交互而且自動處理全部與幾何形性質相關的事物的代碼。這種可替代性(substitutability)是OOP中最強有力的概念之一。

 

Booch對對象提出了一個更加簡潔的描述:

對象具備狀態、行爲和標識。這意味着每個對象均可以擁有內部數據(它們給出了該對象的狀態)和方法(它們產生行爲),而且每個對象均可以擁有內部數據(它們給出了該對象的狀態)和方法(它們產生行爲),而且每個對象均可以惟一地與其餘對象區分開來,具體說來,就是每個對象在內存中都有一個惟一的地址。

 

1.2  每一個對象都有一個接口

亞里士多德大概是第一位深刻研究類型(type)的哲學家,他曾經提出過魚類和鳥類這樣的概念。全部的對象都是惟一的,但同時也是具備相同的特性和行爲的對象所歸屬的類的一部分。

 

 

類描述了具備相同特性(數據元素)和行爲(功能)的對象集合,因此一個類實際上就是一個數據類型,例如全部浮點型數字具備相同的特性和行爲集合。二者的差別在於,程序員經過定義類型適應問題,而再也不被迫只能使用現有的用來表示機器中的存儲單元的數據類型。

 

面向對象程序設計的挑戰之一,就是問題空間的元素和解空間的對象之間建立一對一的映射(意思就是類型定義要準確 )

 

可是,怎樣才成得到有用的對象呢?必須有某種方式產生對對象的請求,使對象完成各類任務。每一個對象都只能知足某些請求,這些請求由對象的接口(interface)所定義,決定接口的即是類型。

接口肯定了對某一特定對象所能發出的請求。可是,在程序中必須有知足這些請求的代碼。這些代碼與隱藏的數據一塊兒構成了實現。從過程型編程的觀點來看,這並不太複雜。在類型中,每個可能的請求都有一個方法與之相關聯,當向對象發送請求時,與之相關聯的方法就會被調用。此過程一般被歸納爲:向某個對象」發送消息「(產生請求),這個對象便知道此消息的目的,而後執行對應的程序代碼。

 

1.3每一個對象都提供服務(實現類型抽象的思路)

     當正在試圖開發或理解一個程序設計時,最好的方法之一就是將對象想象爲」服務提供者「。程序自己將向用戶提供服務,它將經過調用其餘對象提供服務來實現這一目的。你的目的就是去建立(或者最好是在現有代碼庫中尋找)可以提供理想的服務來解決問題的一系列對象。

     着手從事這件事的一種方式就是問一下本身:」若是我能夠將問題從表象中抽取出來,那麼什麼樣的對象能夠立刻解決個人問題呢?「例如,假設你正在建立一個簿記系統,那麼能夠想象,系統應該具備某些包括了預約義的簿記輸入屏幕的對象,一個執行簿記計算的對象集合,以及一個處理在不一樣的打印機上打印和開  的對象。也許上述對象中的某些已經存在了,可是對於那些並不存在的對象,它們看起來像什麼樣子?它們可以提供哪些服務?它們須要哪些對象才能履行它們的義務?若是持續這樣作,那麼最終你會說」那個對象看起來如此簡單,能夠坐下來寫代碼了「,或者會說」我確定那個對象已經存在了「。這是將問題分解爲對象集合的一種合理方式。

     將對象看做是服務提供者還有一個附帶的好處:它有助於提升對象的內聚性。高內聚是軟件設計的基本質量要求之一:這意味着一個軟件構件(例如一個對象,固然它也有多是指一個方法或者一個對象庫)的各個方面」組合「的很好。人們在設計對象時所面臨的一個問題是,將過多瞭解全部的格式和打印技術。你可能會發現,這些功能對於一個對象來講太多了,你須要的是三個甚至更多個對象,其中,一個對象能夠是全部可能的排版的目錄,它能夠被用來查詢有關如何打印一張的信息;另外一個對象(或對象集合)能夠是一個通用的打印接口,它知道有關全部不一樣類型的打印機的信息(可是不包含任何有關簿記的內容,它更應該是一個須要購買而不是本身編寫的對象);第三個對象經過調用另外兩個對象的服務來完成打印任務。這樣,每一個對象都有一個它所能提供的內聚的集合。在良好的面向對象設計中,每一個對象均可以很好地完成一項任務,可是它並不試圖作更多的事。就像在這裏看到的,不只容許經過購買得到某些對象(打印機接口對象),並且還能夠建立可以在別處複用的新對象(排版目錄對象)。

     將對象做爲服務提供者看待是一件偉大的簡化工具,這不只在設計過程當中很是有用,並且當其餘人試圖理解你的代碼或重用某個對象時,若是它們看出了整個對象所能提供的服務的價值,它會使調整對象以適應其設計的過程變得簡單得多。

 

1.4被隱藏的具體實現

     將程序開發人員按照角色分爲類建立者(那些建立新數據類型的程序員)和客戶端程序員(那些在其應用中使用數據類型的類消費者)是大有裨益的。客戶端程序員的目標是收集各類用來實現快速應用開發的類。類建立者的目標是構建類,這種類只向客戶端程序員暴露必需的部分,而隱藏其餘部分。爲何要這樣呢?由於若是加以隱藏,那麼客戶端程序員將不可以訪問它,這意味着類建立者能夠任意修改被隱藏的部分,而不用擔憂對其餘任何人形成影響。被隱藏的部分一般表明對象內部脆弱的部分,它們很容易被粗心的或者不知內情的客戶端程序員所毀壞,所以將實現隱藏起來能夠減小程序bug。

     在任何相互關係中,具備關係所涉及的各方都遵照的邊界是十分重要的事情。當建立一個類庫時,就創建了與客戶端程序員之間的關係,他們一樣也是程序員,可是他們是使用你的類庫來構建應用、或者構建更大的類庫的程序員。若是全部的類成員對任何人都是可用的,那麼客戶端程序員就能夠對類作任何事情,而不受任何約束。即便你但願客戶端程序員不要直接操做你的類中的某些成員,可是若是沒有任何訪問控制,將沒法阻止此事發生。全部東西都將赤裸裸地暴露於世人面前。

所以,訪問控制的第一個存在緣由就是讓客戶端程序員沒法觸及他們不該該觸及的部分-------這些部分對數據類型的內部操做來講是必需的,但並非用戶解決待定問題所需的接口的一部分。這對客戶端程序員來講實際上是一項服務,所以他們能夠很容易地看出哪些東西對他們來講很重要,而哪些東西能夠忽略。

     訪問控制的第二個存在緣由就是容許庫設計者能夠改變類內部的工做方式而不用擔憂會影響到客戶端程序員。例如,你可能爲了減輕開發任務而以某種簡單的方式實現了某個特定類,但稍後發現你必須改寫它才能使其運行得更快。若是接口和實現能夠清晰地分離並得以保護,那麼你就能夠垂手可得地完成這項工做。

     java用三個關鍵字在類的內部設定邊界:public、private、protected。這些訪問指定詞(access specifier)決定了緊跟其後被定義的東西能夠被誰使用。public表示緊隨其後的元素對任何人都是可用的,而private整個關鍵字表示除類型建立者和類型的內部方法以外的任何人都不能訪問的元素。private就像你與客戶端程序員之間的一堵牆,若是有人試圖訪問private成員,就會在編譯時獲得錯誤信息。protected關鍵字與private做用至關,差異僅在於繼承的類能夠訪問protected成員,可是不能訪問private成員。稍後將會對繼承進行介紹。

     java還有一種默認的訪問權限,當沒有使用前面提到的任何訪問指定詞時,它將發揮做用。這種權限一般被稱爲包訪問權限,由於在這種權限下,類能夠訪問在同一個包(庫構件)中的其餘類的成員,可是在包以外,這些成員如同指定了private同樣。

 

1.5複用具體實現

     一旦類被建立並被測試完,那麼它就應該(在理想狀況下)表明一個有用的代碼單元。事實證實,這種複用性並不容易達到咱們所但願的那種程度,產生一個可複用的對象設計須要豐富的經驗和敏銳的洞察力。可是一旦你有了這樣的設計,它就可供複用。代碼複用時面向對象程序設計語言所提供的最了不得的優勢之一。

     最簡單地複用某個類的方式就是直接使用該類的一個對象,此外也能夠將那個類的一個對象置於某個新的類中。咱們稱其爲」建立一個成員對象「。新的類能夠由任意數量、任意類型的其餘對象以任意能夠實現新的類中想要的功能的方式所組成。由於是在使用現有的類合成新的類,因此這種概念被稱爲組合(composition),若是組合是動態發生的,那麼它一般被稱爲聚合(aggregateion)。組合常常被視爲」has-a「(擁有)關係,就像咱們常說的」汽車擁有引擎「同樣。

 

     組合帶來了極大的靈活性。新類的成員對象一般都被聲明爲private,使得使用新類的客戶端程序員不能訪問他們。這也是得你能夠在不干擾現有客戶端代碼的狀況下,修改這些成員。也能夠在運行時修改這些成員對象,以實現動態修改程序的行爲。下面將要討論的繼承並不具有這樣的靈活性,由於編譯器必須對經過繼承而建立的類施加編譯時的限制。

     因爲繼承在面向對象程序設計中如此重要,因此它常常被高度強調,因而程序員新手就會有這樣的印象:到處都應該使用繼承。這會致使難以使用並過度複雜的設計。實際上,在創建新類時,應該首先考慮組合,由於它更加簡單靈活。若是採用這種方式,設計會變得更加清晰。一旦有了一些經驗以後,便可以看出必須使用繼承的場合了。

 

1.6繼承

     對象這種觀念,自己就時十分方便的工具,使得你能夠經過概念將數據和功能封裝到一塊兒,所以能夠對問題空間的觀念給出恰當的表示,而不用受制於必須使用底層機器語言。這些概念用關鍵字class來表示,它們造成了編程語言中的基本單位。

     遺憾的是,這樣作仍是有不少麻煩:在建立了一個類以後,即便另外一個新類與其具備類似的功能,你仍是得從新建立一個新類。若是咱們可以以現有的類爲基礎,複製它,而後經過添加和修改整個副原本建立新類那就要好多了。經過繼承即可以達到這樣的效果,不過也有例外,當源類(被稱爲基類、超類或者父類)發生變更時,被修改的」副本「(被稱爲導出類、繼承類或者子類)也會反映出這些變更。

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

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

第二個例子是經典的幾何形的例子,這在計算機輔助設計系統或遊戲仿真系統中可能被用到。基類是幾何形,每個幾何形都具備尺寸、顏色、位置等,同時每個幾何形均可以被繪製、擦除、移動和着色等。在此基礎上,能夠導出(繼承出)具體的幾何形狀------圓形、正方形、三角形等-------每一種都具備額外的特性和行爲,例如某些形狀能夠被翻轉。某些行爲可能並不相同,例如計算幾何形狀的面積。類型層次結構同時體現了幾何形狀之間的類似性和差別性

     以一樣的術語將解決方案轉換成問題是大有裨益的,由於不須要在問題描述和解決方案描述之間創建許多中間模型。經過使用對象,類型層次結構成爲了主要模型,所以,能夠直接從真實世界中對系統的描述過渡到用代碼對系統進行描述。事實上,對使用面向對象設計的人們來講,困難之一是從開始到結束過於簡單。對於訓練有素、善於尋找複雜的解決方案的頭腦來講,可能會在一開始被這種簡單性給難道。

     當繼承現有類型時,也就創造了新的類型。這個新的類型不只包括現有類型的全部成員(儘管private成員被隱藏了起來,而且不可訪問),並且更重要的是它複製了基類的接口。也就是說,全部能夠發送給基類對象的消息同時也能夠發送給導出類對象。因爲經過發送給類的信息的類型可知類的類型,因此這也就意味着導出類與基類具備相同的類型。在前面的例子中,」一個圓形也就是一個幾何形「。經過繼承而產生的類型等價性是理解面向對象程序設計方法內涵的重要門檻。

     因爲基類和導出類具備相同的基礎接口,因此伴隨此接口的一定有某些具體實現。也就是說,當對象接收到特定消息時,必須有某些代碼去執行。若是隻是簡單地繼承一個類而並不作其餘任何事,那麼在基類接口中的方法將會直接繼承到導出類中。這意味着導出類的對象不只與基類擁有相同的類型,並且還擁有相同的行爲,這樣作沒有什麼特別意義。

     有兩種方法可使基類和導出類產生差別。第一種方法很是直接:直接在導出類中添加新方法。這些新方法並非基類接口的一部分。這意味着基類不能直接知足你的全部需求,所以必需添加更多的方法。這種對繼承簡單而基本的使用方式,有時對問題來講確實使一種完美的解決方式。可是,應該仔細考慮是否存在基類也須要這些額外方法的可能性。這種設計的發現與迭代過程在面向對象程序設計中會常常發生。

     雖然繼承有時可能意味着在接口中添加新方法(尤爲是在以extends關鍵字表示繼承的java中),但並不是總須要如此。第二種也是更重要的一種使導出類和基類之間產生差別的方法是改變現有基類的方法的行爲,這被稱之爲覆蓋(overridiing)那個方法。

要想覆蓋某個方法,能夠直接在導出類種建立該方法的新定義便可。你能夠說:」此時,我正在使用相同的接口方法,可是我想在新類型種作些不一樣的事情。「

 

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來編寫一個方法(後邊很快你就會學習若是編寫):

如上圖,建基類(幾何形),定義方法;依次建裏導出類  圓形 、三角形、等等

     把導出類看作是它的基類的過程稱爲向上轉型(upcasting)。轉型(cast)這個名稱的靈感來自於模型鑄造動做;而向上(up)這個詞來源於繼承圖的典型佈局方式:一般基類在頂部,而導出類在其下部散開。所以,轉型爲一個基類就是在繼承圖中向上移動,即」向上轉型「

     一個面向對象程序確定會在某處包含向上轉型,所以這正是將本身從必須知道確切類型中解放出來的關鍵,讓咱們再看看doSomething()中的代碼:

shape.erase();

shape.draw();

     注意這些代碼並非說」若是是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++的重要改進之一。因爲全部對象都保證具備其類型信息,所以不會因沒法肯定對象的類型而陷入僵局。這對於系統級操做(如異常處理)顯得尤爲重要,而且給編程帶來了更大的靈活性。

 

    1.9 容器

     一般說來,若是不知道在解決某個特定問題時須要多少個對象,或者它們將存活多久,那麼就不可能知道如何存儲這些對象。如何才能知道須要多少空間來建立這些對象呢?結果是你不可能知道,由於這類信息只有在運行時才能得到。

     對於面向對象設計中的大多數問題而言,這個問題的解決方案彷佛過於輕率:建立另外一種對象類型。這種新的對象類型持有對其餘對象的引用。固然,你能夠用在大多數語言中都有的數組類型來實現相同的功能。可是這個一般被稱爲容器(也稱爲集合,不過Java類庫以不一樣的含義使用」集合「這個術語,因此本書將使用」容器「這個詞)的新對象,在任何須要時均可擴充本身以容納你置於其中的全部東西。所以不須要知道未來會把多少個對象置於容器中,只須要建立一個容器對象,而後讓它處理全部細節。

     幸運的是,好的OOP語言都有一組容器,它們做爲開發包的一部分。在C++中,容器是標準C++類庫的一部分,常常被稱爲標準模板類庫(Standard Template Library,STL)。Object Pascal在其可視化構件庫(Visual Component Library,VCL)中有容器;Smalltalk提供了一個很是完備的容器集;Java在其標準類庫中也包含有大量的容器。在某些類庫中,一兩個通用容器足夠知足全部的須要;可是在其餘類庫(例如Java)中,具備知足不一樣須要的各類類型的容器,例如List(用於存儲序列),Map(也被稱爲關聯數組,用來創建對象之間的關聯),Set(每種對象類型只持有一個),以及諸如隊列、樹、堆棧等更多的構件。

     從設計的觀點來看,真正須要的只是一個能夠被操做,從而解決問題的序列。若是單一類型的容器能夠知足全部須要,那麼就沒有理由設計不一樣種類的序列了。然而仍是須要對容器有所選擇,這有兩個緣由。第一,不一樣容器提供了不一樣類型的接口和外部行爲。堆棧相比於隊列就具有不一樣的接口和行爲,也不一樣於集合和列表的接口和行爲。它們之中的某種容器提供的解決方案可能比其餘容器要靈活的多。第二,不一樣的容器對於某些操做具備不一樣的效率。最好的例子就是兩個List的比較:ArrayList和LinkedList。它們都是具備相同接口和外部行爲的簡單的序列,可是它們對某些操做所花費的代價卻有天壤之別。在ArrayList中,隨機訪問元素是一個花費固定時間的操做;可是,對LinkedList來講,隨機選取元素須要在列表中移動,這種代價是高昂的,訪問越靠近表尾的元素,花費的時間越長。而另外一方面,若是想在序列中間插入一個元素,LinkedList的開銷卻比ArrayList要小。上述操做以及其餘操做的效率,依序列底層結構的不一樣而存在很大的差別。咱們能夠在一開始使用LinkedList構建程序,而在優化系統性能時改用ArrayList。接口List所帶來的抽象,把在容器之間進行轉換時對代碼產生的影響降到最小限度。

 

1.9.1 參數化類型

      在Java SE5出現以前,容器存儲的對象都只具備Java中的通用類型:Object。單根繼承結構意味着全部東西都是Object類型,因此能夠存儲Object的容器能夠存儲任何東西。這使得容器很容易被複用。

     要使用這樣的容器,只需在其中置入對象引用,稍後還能夠將它們取回。可是因爲容器只存儲Object,因此當將對象引用置入容器時,它必須被向上轉型爲Object,所以它會丟失其身份。當把它取回時,就獲取了一個Object對象的引用,而不是對置入時的那個類型的對象的引用。因此,怎樣才能將它變回先前置入容器中時的具備實用接口的對象呢?

     這裏再度用到了轉型,但這一次不是向繼承結構的上層轉型爲一個更泛化的類型,而是向下轉型爲更具體的類型。這種轉型的方式稱爲向下轉型。咱們知道,向上轉型是安全的,例如Circle是一種Shape類型;可是不知道某個Object是Circle仍是Shape,因此除非確切知道所要處理的對象的類型,不然向下轉型幾乎是不安全的。

     然而向下轉型並不是完全是是危險的,由於若是向下轉型爲錯誤的類型,就會獲得被稱爲異常的運行時錯誤,稍後會介紹什麼時異常。儘管如此,當從容器中取出對象引用時,仍是必需要以某種方式記住這些對象究竟時什麼類型,這樣才能執行正確的向下轉型。

     向下轉型和運行時的檢查須要額外的程序運行時間,也須要程序員付出更多的心血。那麼建立這樣的容器,它知道本身所保存的對象的類型,從而不須要向下轉型以及消除犯錯誤的可能,這樣不是更有意義嗎?這種解決方案被稱爲參數化類型機制。參數化類型就是一個編譯器能夠自動定製做用於特定類型上的類。例如,經過使用參數化類型編譯器能夠定製一個只接納和取出Shape對象的容器。

     Java SE5的重大變化之一就是增長了參數化類型,在java中它稱爲泛型。一對尖括號,中間包含類型信息,經過這些特徵就能夠識別對泛型的使用。例如,能夠用下面這樣的語句來建立一個存儲Shape的ArrayList:

ArrayList<Shape> shapes = new ArrayList<Shape>();

     爲了利用泛型的優勢,不少標準類庫構件都已經進行了修改。就像咱們將要看到的那樣,泛型對本書中的許多代碼都產生了重要的影響。

 

1.10 對象的建立和生命期

     在使用對象時,最關鍵的問題之一即是它們的生成和銷燬方式。每一個對象爲了生存都須要資源,尤爲時內存。當咱們再也不須要一個對象時,它必須被清理掉,使其佔有的資源能夠被釋放和重用。在相對簡單的編程狀況下,怎樣清理對象看起來彷佛不是什麼挑戰:你建立了對象,根據須要使用它,而後它應該被銷燬。然而,你極可能會遇到相對複雜的狀況。

     例如,假設你正在爲某個機場設計空中交通管理系統(一樣的模型在倉庫貨櫃管理系統、錄像帶出租系統或寵物寄宿店也適用)。一開始問題彷佛很簡單:建立一個容器來保存全部的飛機,而後爲每一架進入空中交通控制區的飛機建立一個新的飛機對象,並將其置於容器中。對於清理工做,只需在飛機離開此區域時刪除相關的飛機對象便可。

     可是,可能還有別的系統記錄着有關飛機的數據,也許這些數據不須要像主要控制功能那樣當即引人注意。例如,它可能記錄了全部飛離機場的小型飛機的飛行計劃。所以你須要有第二個容器來存放小型飛機;不管什麼時候,只要建立的使小型飛機對象,那麼它同時也應該置入第二個容器內。而後某個後臺進程在空閒時對第二個容器內的對象進行操做。

     如今問題變得更困難了:怎樣才能知道什麼時候銷燬這些對象呢?當處理完某個對象以後,系統某個其餘部分可能還在處理它。在其餘許多場合中會遇到一樣的問題,在必須明確刪除對象的編程系統中(例如C++),此問題會變得十分複雜。

     對象的數據位於何處?怎樣控制對象的生命週期?C++認爲效率控制是最重要的議題,因此給程序員提供了選擇的權利。爲了追求最大的執行速度,對象的存儲空間和生命週期能夠在編寫程序時肯定,這能夠經過將對象置於堆棧(它們有時被爲自動變量 (automatic variable)或限域變量(scoped variable))或靜態存儲區域內來實現。這種方式將存儲空間分配和釋放置於優先考慮的位置,某些狀況下這樣控制很是有價值。可是,也犧牲了靈活性,由於必須在編寫程序時知道對象確切的數量、生命週期和類型。若是試圖解決更通常化的問題,例如計算機輔助設計、倉庫管理或者空中交通控制,這種方式就顯得過於受限了。

     第二種方式是在被稱爲堆(heap)的內存池中動態地建立對象。在這種方式中,直到運行時才知道須要多少對象,它們的生命週期如何,以及它們的具體類型時什麼。這些問題的結果只能在程序運行時相關代碼被執行到的那一刻才能肯定。若是須要一個新對象,能夠在須要的時刻直接在堆中建立。由於存儲空間是在運行時被動態管理的,因此須要大量的時間在堆中分配存儲空間,這可能要遠遠大於在堆棧中建立存儲空間的時間。在堆棧中建立存儲空間和釋放存儲空間一般各須要一條彙編指令便可,分別對應將棧頂指針向下移動和將棧頂指針向上移動。建立堆存儲空間的時間依賴於存儲機制的設計。

     動態方式有這樣一個通常性的邏輯假設:對象趨向於變得複雜,因此查找和釋放存儲空間的開銷不會對對象的建立形成重大沖擊。動態方式所帶來的更大的靈活性正是解決通常化編程問題的要點所在。

     java徹底採用了動態內存分配方式。每當想要建立新對象時,就要使用new關鍵字來構建此對象的動態實例。

     還有一個議題,就是對象生命週期。對於容許在堆棧上建立對象的語言,編譯器能夠肯定對象存活的時間,並能夠自動銷燬它。然而,若是是在堆上建立對象,編譯器就會對它的生命週期一無所知。在像C++這樣的語言中,必須經過編程方式來肯定什麼時候銷燬對象,這可能會由於不能正確處理而致使內存泄露(這在C++程序中是常見的問題)。Java提供了被稱爲「垃圾回收器」的機制,它能夠自動發現對象什麼時候再也不被使用,並繼而銷燬它。垃圾回收器很是有用,由於它減小了所必須考慮的議題和必須編寫的代碼。更重要的是,垃圾回收器提供了更高層的保障,能夠避免暗藏的內存泄露問題,這個問題已經使許多C++項目折戟沉沙。

     Java的垃圾回收器被設計用來處理內存釋放問題(儘管它不包括清理對象的其餘方面)。垃圾回收器「知道」對象什麼時候再也不被使用,而且自動釋放對象佔用的內存。這一點同全部對象都是繼承自單根基類Object以及只能以一種方式建立對象(在堆上建立)這兩個特性結合起來,使得用Java編程的過程較之用C++編程要簡單得多,所要作出的決策和要克服的障礙也少得多。

 

1.11 異常處理:處理錯誤

     自從編程語言問世以來,錯誤處理就始終是最困難的問題之一。由於設計一個良好的錯誤處理機制很是困難,因此許多語言直接略去這個問題,將其交給程序庫設計者處理,而這些設計者也只是提出了一些不完全的方法,這些方法可用於許多很容易就能夠繞過此問題的場合,並且其解決方式一般也只是忽略此問題。大多數錯誤處理機制的主要問題在於,它們都依賴於程序員自身的警戒性,這種警戒性來源於一種共同的約定,而不是編程語言所強制的。若是程序員不夠警戒------一般是由於他們太忙,這些機制就很容易被忽視。

相關文章
相關標籤/搜索