Java編程思想 ——對象導論

抽象過程

所有編程語言都是抽象機制。人們所能夠解決的問題的複雜性,直接取決於抽象的類型和數量。彙編語言是對底層機器的輕微抽象;“命令式語言”(BASIC、C)是對彙編語言的抽象。

它們所作的主要抽象仍要求在解決問題時要基於計算機的結構,而不是基於所要解決的問題的結構來考慮。程序員必須建立起在 機器模型(解空間,對問題建模的地方——計算機)和實際待解決問題的模型(問題空間,問題存在的地方——一項業務)之間的關聯。

另一種對機器建模的方式是只針對待解決問題建模。OOP允許根據問題來描述問題,而不是根據運行方案的計算機來描述問題。但是它仍然與計算機有聯繫:每個對象看起來有點像一個微型計算機——它具有狀態,還具有操作。

複用具體實現

使用現有的類合成新的類,稱爲組合(composition)

組合具有極大的靈活性,可以在運行時修改成員對象,以實現動態修改程序的行爲。繼承並不具備這樣的靈活性,編譯器必須對繼承而創建的類施加編譯時的限制。

在建立新類時,應該首先考慮組合,因爲它更加簡單靈活。

伴隨多態的可互換對象

應用場景

在處理類型的層次結構時,想把一個對象不當成它所屬的特定類型對待,而是將其作爲基類的對象來對待,可以編寫出不依賴於特定類型的代碼。這樣的代碼不會受添加新類型影響。

問題

試圖將導出類型的對象,當作泛化基類型來看待時,編譯器在編譯時是不可能知道應該執行哪一段代碼的。例如把自行車看成交通工具,讓交通工具「行駛」,只有在知道這個交通工具是自行車的情況下才有可能。

當發送這樣的消息時,程序員並不想知道哪一段代碼將被執行;對象會依據自身的具體類型來執行恰當的代碼。

前期綁定、後期綁定

編譯器不可能產生傳統意義上的函數調用。一個非面向對象編程的編譯器,產生的函數調用會引起前期綁定。編譯器將產生對一個具體函數名字的調用,而運行時將這個調用解析到將要被執行的代碼的絕對地址。但是在OOP中,程序直到運行時才能夠確定代碼的地址,所以當消息發送到一個泛化對象時,必須採用其他的機制。

爲了解決這個問題,面向對象程序設計語言使用了後期綁定的概念。當向對象發送消息時,被調用的代碼直到運行時才能確定。編譯器確保被調用方法的存在,並對調用參數和返回值執行類型檢查,但是並不知道被執行的確切代碼。

爲了執行後期綁定,Java使用一小段特殊的代碼替代絕對地址調用,這段代碼使用在對象中存儲的信息來計算方法體的地址。

C++:必須明確地聲明希望某個方法具備綁定屬性所帶來的靈活性。方法在默認情況下不是動態綁定的。 Java:動態綁定是默認行爲,不需要添加額外的關鍵字來實現多態。

示例

doSomething方法可以與任何Shape對話。

void doSomething(Shape shape){
    shape.erase();
    shape.draw();
}
Circle circle = new Circle();
Line line = new Line();

doSomething(circle);
doSomething(line);

向上轉型

把將導出類看做是它的基類的過程稱爲向上轉型。當Java編譯器在編譯doSomething()的代碼時,並不能確切知道doSomething()要處理的確切類型,所以期望調用基類Shape的erase()版本。

單根繼承結構

除C++以外的所有OOP語言,所有的類最終都繼承自單一的基類。

好處

  1. 單根繼承結構保證所有對象都具有一個共用接口,所以它們歸根到底都是相同的基本類型。
  2. 單根繼承結構保證所有對象都具備某些功能。
  3. 單根繼承結構使垃圾回收器的實現變得容易很多。

容器

爲什麼需要容器?

如果不知道在解決某個特定問題時,需要多少個對象,或者它們將存活多久,那麼就不可能知道如何存儲對象。如何才能知道需要多少空間來創建這些對象呢?——你不可能知道,這類信息只有在運行時才能獲得。

什麼是容器?

一種對象類型,持有對其他對象的引用。在任何需要時都可以擴充自己,以容納所有東西。

Java類庫的容器

  • List:用於存儲序列
  • Map:關聯數組,用來建立對象之間的關聯
  • Set:每種對象類型只有一個

爲什麼需要多種容器?

  1. 不同容器提供了不同類型的接口和外部行爲。
  2. 不同容器對於某些操作具有不同的效率。最好的例子就是兩種List:ArrayList和LinkedList。接口List所帶來的抽象,把在容器間進行轉換時對代碼產生的影響降到最小限度。

參數化類型

Java SE5之前

容器存儲的對象都是Java的通用類型:Object。單根繼承結構意味着所有東西都是Object類型,所以可以存儲Object的容器可以存儲任何東西。

但是由於容器只存儲Object,所以當將對象引用置入容器時,它必須被向上轉型爲Object,會丟失身份。當把它取回時,就獲取了一個對Object對象的引用,而不是具體類型的對象的引用。

這次轉型不是向繼承結構的上層轉型爲一個更泛化的類型,而是向下轉型爲更具體的類型——向下轉型。向上轉型是安全的,向下轉型是不安全的

Java SE5之後

如何創建容器,使它知道自己所保存的對象的類型,從而不需要向下轉型以及消除犯錯誤的可能?——參數化類型機制。參數化類型是一個編譯器可以自動定製作用於特定類型上的類。在Java中成爲泛型,使用一對尖括號,中間包含類型信息。

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

對象的創建和生命週期

使用對象,最關鍵的問題是生成和銷燬的方式,因爲每個對象都需要佔用內存。當我們不需要一個對象時,它必須被清理掉,使其佔有的資源可以被釋放和重用。

C++

爲了追求最大的執行速度,對象的存儲空間和生命週期可以在編寫程序時確定,通過將對象置於堆棧或靜態存儲區域內實現。

這種方式將存儲空間分配和釋放放在最優先的位置,但是犧牲了靈活性,因爲必須在編寫程序時知道對象確切的數量、生命週期和類型。

Java

堆(heap)的內存池動態地創建對象。在這種方式中,直到運行時才知道需要多少對象,它們的生命週期如何,以及它們的具體類型是什麼。這些問題只能在程序運行時相關代碼被執行到的那一刻才能確定。

如果需要一個新對象,可以在需要的時刻直接在堆中創建。因爲存儲空間是在運行時被動態管理的,所以需要大量的時間在堆中分配存儲空間,這可能>>在堆棧中(C++)創建存儲空間的時間。

動態方式基於一個一般性的邏輯假設:對象趨於複雜,所以查找和釋放存儲空間的開銷不會對對象的創建造成重大沖擊。Java完全採用了動態內存分配方式。

異常處理:處理錯誤

異常處理就像是與程序正常執行路徑並行的、在錯誤發生時執行的另一條路徑。因爲它是另一條完全分離的執行路徑,所以它不會干擾正常的執行代碼。

Java的異常處理

Java一開始就內置了異常處理,而且強制你必須使用它。它是唯一可接受的錯誤報告方式。如果沒有編寫正確的處理異常的代碼,那麼就會得到一條編譯時的錯誤。這種有保障的一致性有時會使得錯誤處理變得非常容易。

併發編程

如何在同一時刻處理多個任務?把問題切分成多個可獨立運行的部分,從而提高程序的響應能力。在程序中,這些彼此獨立運行的部分稱爲線程,上述概念稱爲“併發”。

線程只是一種爲單一處理器分配執行時間的手段。但是如果操作系統支持多處理器,那麼每個任務都可以被指派給不同的處理器,並且它們是在真正地並行執行。在語言級別上,多線程使得程序員不再操心機器是多處理器還是一個處理器。

併發的隱患

共享資源。如果有多個並行任務都要訪問同一個資源,就會出問題。對於共享的資源,必須在使用期間被鎖定

總結

過程型語言:數據定義和函數調用。

因爲OOP在你能夠在過程型語言中找到的概念的基礎上,又添加了許多新概念,所以你可以假設:由此而產生的Java程序比等價的過程型程序要複雜得多。但是你會感到很驚喜:編寫良好的Java程序通常比過程型程序要簡單得多,而且易於理解得多。

你只需要兩部分內容的定義:用來表示問題空間概念的對象,發送給這些對象的用來表示在此空間內的行爲的消息。許多問題都可以通過重用現有的類庫代碼而得到解決。

即使最終仍舊選擇Java作爲編程語言,至少也要理解還有哪些選項可供選擇,並且對爲什麼選擇這個方向要有清楚的認識。

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