第3章:抽象數據類型(ADT)和麪向對象編程(OOP) 3.4面向對象編程(OOP)

大綱

面向對象的標準
基本概念:對象,類,屬性,方法和接口
OOP的獨特功能程序員

封裝和信息隱藏
繼承和重寫
多態性,子類型和重載
靜態與動態分派

Java中一些重要的Object方法
設計好的類
面向對象的歷史
總結算法

面向對象的標準

面向對象的編程方法/語言應該具備類的概念做爲中心概念。
語言應該可以爲類和它的特徵提供斷言(即規範:前置條件,後置條件和不變量)和異常處理,依靠工具生成這些斷言中的文檔,而且可選地在運行時監視它們 時間。數據庫

  • 他們幫助生產可靠的軟件;
  • 他們提供系統文件;
  • 它們是測試和調試面向對象軟件的核心工具。

靜態類型:一個定義良好的類型系統應該經過強制執行一些類型聲明和兼容性規則來保證它接受的系統的運行時類型安全。編程

泛型(Genericity):用於「準備改變」和「爲/重用設計」:應該能夠編寫具備表示任意類型的正式泛型參數的類。數組

繼承(Inheritance):應該能夠將一個類定義爲從另外一個繼承,以控制潛在的複雜性。安全

多態(Polymorphism):在基於繼承的類型系統的控制下,應該能夠將實體(表示運行時對象的軟件文本中的名稱)附加到各類可能類型的運行時對象。模塊化

動態分派/綁定(Dynamic dispatch / binding):在一個實體上調用一個特性應該老是觸發與所附加的運行時對象的類型相對應的特性,這在調用的不一樣執行過程當中不必定是相同的。函數

基本概念:對象,類,屬性和方法

對象工具

真實世界的物體有兩個特徵:它們都有狀態和行爲。
識別真實世界對象的狀態和行爲是從OOP角度開始思考的好方法。性能

  • 狗有狀態(名稱,顏色,品種,飢餓)和行爲(吠叫,取出,搖尾巴)。
  • 自行車具備狀態(當前檔位,當前踏板節奏,當前速度)和行爲(更換檔位,改變踏板節奏,應用制動器)。

對於你看到的每一個對象,問本身兩個問題,這些現實世界的觀察都轉化爲OOP的世界:

  • 這個物體有什麼可能的狀態?
  • 這個對象有什麼可能的行爲?

一個對象是一組狀態和行爲

狀態 - 包含在對象中的數據。

  • 在Java中,這些是對象的字段

行爲 - 對象支持的操做

  • 在Java中,這些被稱爲方法
  • 方法只是面向對象的功能
  • 調用一個方法=調用一個函數

每一個對象都有一個類

  • 一個類定義方法和字段
  • 統稱爲成員的方法和領域

類定義了類型和實現

  • 類型≈可使用對象的位置
  • 執行≈對象如何作事情

鬆散地說,類的方法是它的應用程序編程接口(API)

  • 定義用戶如何與實例進行交互

靜態與實例變量/類的方法

類成員變量:與類相關聯的變量,而不是類的實例。 您還能夠將方法與類關聯 - 類方法。

  • 要引用類變量和方法,須要將類的名稱和類方法或類變量的名稱連同句點('.')一塊兒加入。

不是類方法或類變量的方法和變量稱爲實例方法和實例成員變量。

  • 要引用實例方法和變量,必須引用類實例中的方法和變量

總結:

  • 類變量和類方法與類相關聯,而且每一個類都會出現一次。 使用它們不須要建立對象。
  • 實例方法和變量在類的每一個實例中出現一次。

靜態方法不與任何特定的類實例關聯,而實例方法(不帶static關鍵字聲明)必須在特定對象上調用。

接口

Java的接口是一種用於設計和表達ADT的有用語言機制,其實現方式是實現該接口的類。

  • Java中的接口是方法簽名的列表,但沒有方法體。
  • 若是一個類在其implements子句中聲明接口併爲全部接口的方法提供方法體,則該類將實現一個接口。
  • 一個接口能夠擴展一個或多個其餘接口
  • 一個類能夠實現多個接口
  • 接口和類:定義和實現ADT
  • 接口之間能夠繼承
  • 一個類能夠實現多個接口

接口和實現

API的多個實現能夠共存

  • 多個類能夠實現相同的API
  • 他們在性能和行爲方面可能有所不一樣

在Java中,API由接口或類指定

  • 接口只提供一個API
  • 一個接口定義但不實現API
  • 類提供了一個API和一個實現
  • 一個類能夠實現多個接口

一個接口能夠有多種實現

Java接口和類

接口與類

  • 接口:肯定ADT規約
  • 類:實現ADT

類確實定義了類型

  • 相似接口方法的公共類方法
  • 可從其餘課程直接訪問的公共字段

但更喜歡使用接口

  • 除非你知道一個實現就足夠,不然使用變量和參數的接口類型。
  • 支持更改實施;
  • 防止依賴於實施細節

問題:打破抽象邊界

  • 客戶必須知道具體表示類的名稱。
  • 由於Java中的接口不能包含構造函數,因此它們必須直接調用其中一個具體類的構造函數。
  • 構造函數的規範不會出如今接口的任何位置,因此沒有任何靜態的保證,即便不一樣的實現甚至會提供相同的構造函數。

接口的優勢

接口指定了客戶端的契約,僅此而已。

  • 接口是客戶程序員須要閱讀才能理解ADT的所有內容。
  • 客戶端不能在ADT的表示上建立無心的依賴關係,由於實例變量根本沒法放入接口。
  • 實現徹底保持徹底分離,徹底不一樣。 抽象數據類型的多個不一樣表示能夠共存於同一個程序中,做爲實現接口的不一樣類。
  • 當一個抽象數據類型被表示爲一個單獨的類時,若是沒有接口,就很難擁有多個表示。

爲何有多個實現

不一樣的表現

  • 選擇最適合您使用的實現

不一樣的行爲

  • 選擇你想要的實現
  • 行爲必須符合接口規範(「契約」)

性能和行爲每每不盡相同

  • 提供功能
  • 性能權衡
  • 示例:HashSet,TreeSet

接口總結

  • 編譯器和人員的文檔
  • 容許性能權衡
  • 可選的方法
  • 有意識欠定規格的方法
  • 一個類的多個視圖
  • 愈來愈不值得信賴的實現

減小錯誤保證安全

  • ADT由它的操做定義,而接口就是這樣作的。
  • 當客戶端使用接口類型時,靜態檢查確保它們只使用接口定義的方法。
  • 若是實現類暴露其餘方法 - 或更糟糕的是,具備可見的表示 - 客戶端不會意外地看到或依賴它們。
  • 當咱們有一個數據類型的多個實現時,接口提供方法簽名的靜態檢查。

容易明白

  • 客戶和維護人員確切知道在哪裏查找ADT的規範。
  • 因爲接口不包含實例字段或實例方法的實現,所以更容易將實現的細節保留在規範以外。

準備好改變

  • 咱們能夠經過添加實現接口的類輕鬆地添加新類型的實現。
  • 若是咱們避免使用靜態工廠方法的構造函數,客戶端將只能看到該接口。
  • 這意味着咱們能夠切換客戶端正在使用的實現類,而無需更改其代碼。

封裝和信息隱藏

信息隱藏

將精心設計的模塊與很差的模塊區分開來的惟一最重要的因素是其隱藏內部數據和其餘模塊的其餘實施細節的程度。
設計良好的代碼隱藏了全部的實現細節

  • 將API與實施徹底分開
  • 模塊只經過API進行通訊
  • 對彼此的內在運做無知

被稱爲信息隱藏或封裝,是軟件設計的基本原則。

信息隱藏的好處

將構成系統的類分開

  • 容許它們獨立開發,測試,優化,使用,理解和修改

加速系統開發

  • 類能夠並行開發

減輕了維護的負擔

  • 能夠更快速地理解類並調試,而沒必要懼怕損害其餘模塊

啓用有效的性能調整

  • 「熱」類能夠單獨優化

增長軟件重用

  • 鬆散耦合的類一般在其餘狀況下證實是有用的

經過接口隱藏信息

使用接口類型聲明變量
客戶端只能使用接口方法
客戶端代碼沒法訪問的字段
但咱們到目前爲止

  • 客戶端能夠直接訪問非接口成員
  • 實質上,它是自願的信息隱藏

成員的可見性修飾符

private - 只能從聲明類訪問
protected - 能夠從聲明類的子類(以及包內)
public - 從任何地方訪問

信息隱藏的最佳實踐

仔細設計你的API
只提供客戶須要的功能,其餘全部成員應該是私人的
您能夠隨時在不破壞客戶的狀況下讓私人成員公開

  • 但反之亦然!

繼承和重寫

(1)重寫

可重寫的方法和嚴格的繼承
可重寫方法:容許從新實現的方法。

  • 在Java方法中默認是可重寫的,即沒有特殊的關鍵字。

嚴格的繼承

  • 子類只能向超類添加新的方法,它不能覆蓋它們
  • 若是某個方法不能在Java程序中被覆蓋,則必須以關鍵字final爲前綴。

final

final字段:防止初始化後從新分配給字段
final方法:防止重寫該方法
final類:阻止繼承類

重寫

方法重寫是一種語言功能,它容許子類或子類提供已由其超類或父類之一提供的方法的特定實現。

  • 相同的名稱,相同的參數或簽名,以及相同的返回類型。
  • 執行的方法的版本將由用於調用它的對象決定。實際執行時調用哪一個方法,運行時決定。
  • 若是父類的對象用於調用該方法,則會執行父類中的版本;
  • 若是使用子類的對象來調用該方法,則會執行子類中的版本。

當子類包含一個覆蓋超類方法的方法時,它也可使用關鍵字super調用超類方法。
重寫的時候,不要改變原方法的本意

(2)抽象類

抽象方法和抽象類

抽象方法:

  • 具備簽名但沒有實現的方法(也稱爲抽象操做)
  • 由關鍵字abstract定義

抽象類:

  • 一個包含至少一個抽象方法的類被稱爲抽象類

接口:只有抽象方法的抽象類

  • 接口主要用於系統或子系統的規範。 該實現由子類或其餘機制提供。

具體類⇒抽象類⇒接口

多態性,子類型和重載

(1)三種多態性

特殊多態(Ad hoc polymorphism):當一個函數表示不一樣且可能不一樣種類的實現時,取決於單獨指定類型和組合的有限範圍。 使用函數重載(function overloading)在許多語言中支持特設多態。
參數化多態(parametric polymorphism):當代碼被寫入時沒有說起任何特定類型,所以能夠透明地使用任何數量的新類型。 在面向對象的編程社區中,這一般被稱爲泛型或泛型編程。
子類型多態(也稱爲子類型多態或包含多態):當名稱表示由一些公共超類相關的許多不一樣類的實例。

(2)特殊多態和重載

當函數適用於幾種不一樣的類型(可能不會顯示公共結構)而且可能以不相關的方式表現每種類型時,能夠得到特殊多態。

(3)重載

重載的方法容許您在類中重複使用相同的方法名稱,但使用不一樣的參數(以及可選的不一樣的返回類型)。
重載方法一般意味着對於那些調用方法的人來講會更好一些,由於代碼承擔了應對不一樣參數類型的負擔,而不是在調用方法以前強制調用方執行轉換。

函數重載能夠在不一樣的實現中建立同名的多個方法。

  • 對重載函數的調用將運行適合於調用上下文的該函數的特定實現,容許一個函數調用根據上下文執行不一樣的任務。

重載是一種靜態多態

  • 函數調用使用「最佳匹配技術」解決,即根據參數列表解析函數。
  • 函數調用中的靜態類型檢查
  • 在編譯時肯定使用這些方法中的哪個。(靜態類型檢查)
  • 與之相反,重寫方法則是在運行時進行動態檢查!

重載規則

函數重載中的規則:重載函數必須因參數或數據類型而有所不一樣

  • 必須改變參數列表。
  • 能夠改變返回類型。
  • 能夠改變訪問修飾符。
  • 能夠聲明新的或更普遍的檢查異常。
  • 一個方法能夠在同一個類或子類中重載。

重寫與重載

不要混淆覆蓋派生類中的方法和重載方法名稱

  • 當方法被覆蓋時,派生類中給出的新方法定義與基類中的參數數量和類型徹底相同
  • 當派生類中的方法與基類中的方法有不一樣的簽名時,即重載
  • 請注意,當派生類重載原始方法時,它仍然繼承基類中的原始方法

(3)參數多態性和泛型編程

當一個函數在一系列類型上統一工做時得到參數多態性; 這些類型一般具備一些共同的結構。

  • 它可以以通用的方式定義函數和類型,以便它能夠在運行時傳遞參數的基礎上工做,即容許在沒有徹底指定類型的狀況下進行靜態類型檢查。
  • 這是Java中所謂的「泛型(Generic)」。

泛型編程是一種編程風格,其中數據類型和函數是根據待指定的類型編寫的,隨後在須要時做爲參數提供的特定類型實例化。

泛型編程圍繞從具體,高效的算法中抽象出來以得到可與不一樣數據表示形式結合的泛型算法來生成各類各樣有用軟件的想法相關。

Java中的泛型

類型變量是一個不合格的標識符。

  • 它們由泛型類聲明,泛型接口聲明,泛型方法聲明和泛型構造函數聲明引入。

若是一個類聲明一個或多個類型變量,則該類是通用的。

泛型類:其定義中包含了類型變量

  • 這些類型的變量被稱爲類的類型參數。
  • 它定義了一個或多個做爲參數的類型變量。
  • 泛型類聲明定義了一組參數化類型,每一個類型參數部分的每一個可能的調用都有一個類型聲明。
  • 全部這些參數化類型在運行時共享相同的類。

若是聲明瞭類型變量,則interface是通用的。

  • 這些類型變量被稱爲接口的類型參數。
  • 它定義了一個或多個做爲參數的類型變量。
  • 通用接口聲明定義了一組類型,每種類型參數部分的每一個可能的調用都有一個類型。
  • 全部參數化類型在運行時共享相同的接口。

若是聲明類型變量,則方法是通用的。

  • 這些類型的變量被稱爲方法的形式類型參數。
  • 形式類型參數列表的形式與類或接口的類型參數列表相同。

類型變量

使用菱形運算符<>來幫助聲明類型變量。

一些Java泛型細節

能夠有多個類型參數

  • 例如Map <E,F>,Map <String,Integer>

Wildcards通配符,只在使用泛型的時候出現,不能在定義中出現

  • List <?> list = new ArrayList <String>();
  • List<? extends Animal>
  • List<? super Animal>

通用類型信息被擦除(即僅編譯時)

  • 不能使用instanceof()來檢查通用類型運行時泛型消失了!

沒法建立通用數組

  • Pair <String> [] foo = new Pair <String> [42]; //不會編譯

(4)子類多態性

子類

一個類型是一組值。

  • Java List類型由接口定義。
  • 若是咱們考慮全部可能的List值,它們都不是List對象:咱們不能建立接口的實例。

相反,這些值都是ArrayList對象或LinkedList對象,或者是實現List的另外一個類的對象。
子類型只是超類型的一個子集

  • ArrayList和LinkedList是List的子類型。

繼承/子類型的好處:重用代碼,建模靈活性
在Java中:每一個類只能直接繼承一個父類; 一個類能夠實現多個接口。

「B是A的子類型」意思是「每一個B都是A.」
在規格方面:「每一個B都符合A的規格」。

  • 若是B的規格至少和A的規格同樣強,B只是A的一個子類型。 - 當咱們聲明一個實現接口的類時,Java編譯器會自動執行這個需求的一部分:它確保A中的每一個方法出如今B中,並帶有一個兼容的類型簽名。
  • B類不能實現接口A,而不實現A中聲明的全部方法。

靜態檢查子類型

但編譯器沒法檢查咱們是否以其餘方式削弱了規範:

  • 增強對某種方法的某些投入的先決條件
  • 弱化後置條件
  • 弱化接口抽象類型通告客戶端的保證。

若是你在Java中聲明瞭一個子類型(例如,實現一個接口),那麼你必須確保子類型的規範至少和超類型同樣強。
子類型的規約不能弱化超類型的規約。

子類型多態

子類型多態:不一樣類型的對象能夠被客戶代碼統一處理子類型多態:不一樣類型的對象能夠統一的處理而無需區分
每一個對象根據其類型行爲(例如,若是添加新類型的賬戶,客戶端代碼不會更改)從而隔離了「變化」

Liskov替換原則(LSP):

  • 若是S是T的子類型,那麼類型T的對象能夠用類型S的對象替換(即,類型T的對象能夠用子類型S的任何對象代替)而不改變T的任何指望屬性。

instanceof

測試某個對象是否爲給定類的運算符
建議:若是可能,避免使用instanceof(),而且從不在超類中使用instanceof()來檢查針對子類的類型。

類型轉換

有時你想要一種不一樣於你已有的類型
若是你知道你有一個更具體的子類型,頗有用
可是若是類型不兼容,它會獲得一個ClassCastException

建議:

  • 避免向下轉換類型
  • 從不在超類內向某個子類降低

動態分派

動態分派是選擇在運行時調用多態操做的哪一個實現的過程。

  • 面向對象的系統將一個問題建模爲一組交互對象,這些對象執行按名稱引用的操做。
  • 多態現象是這樣一種現象,即有些可互換的對象每一個都暴露同一名稱的操做,但行爲可能不一樣。

肯定在運行時要調用哪一種方法,即對重寫或多態方法的調用可在運行時解決

做爲示例,File對象和Database對象都有一個StoreRecord方法,可用於將人員記錄寫入存儲。 他們的實現不一樣。

一個程序持有一個對象的引用,該對象多是一個File對象或一個數據庫對象。 它多是由運行時間設置決定的,在這個階段,程序可能不知道或關心哪個。
當程序在對象上調用StoreRecord時,須要肯定哪些行爲被執行。
該程序將StoreRecord消息發送給未知類型的對象,並將其留給運行時支持系統以將消息分派給正確的對象。 該對象實現它實現的任何行爲。

動態分派與靜態分派造成對比,其中在編譯時選擇多態操做的實現。

動態分派的目的是爲了支持在編譯時沒法肯定多態操做的適當實現的狀況,由於它依賴於操做的一個或多個實際參數的運行時類型。

靜態分派:編譯階段便可肯定要執行哪一個具體操做。
重載的方法使用靜態分派,而重寫的方法在運行時使用動態分派。

動態分派不一樣於動態綁定(也稱爲動態綁定)。

  • 選擇操做時,綁定會將名稱與操做關聯。
  • 在決定了名稱引用的操做後,分派會爲操做選擇一個實現。
  • 綁定:將調用的名字與實際的方法名字聯繫起來(可能不少個);
    分派:具體執行哪一個方法(提早綁定→靜態分派)
  • 經過動態分派,該名稱能夠在編譯時綁定到多態操做,但不會在運行時間以前選擇實現。動態分派:編譯階段可能綁定到多態操做,運行階段決定具體執行哪一個(覆蓋和過載均是如此);
  • 雖然動態分派並不意味着動態綁定,但動態綁定確實意味着動態分派,由於綁定決定了可用分派的集合。

提早/靜態綁定

每當發生靜態,私有和最終方法的綁定時,類的類型由編譯器在編譯時肯定,而且綁定在那裏和那裏發生。

推遲/動態綁定

重寫父類和子類都有相同的方法,在這種狀況下,對象的類型決定了要執行哪一種方法。 對象的類型在運行時肯定。

動態方法分派

1.(編譯時)肯定要查找哪一個類
2.(編譯時)肯定要執行的方法簽名

  • 查找全部可訪問的適用方法
  • 選擇最具體的匹配方法

3.(運行時)肯定接收器的動態類別
4.(運行時)從動態類中,找到要調用的方法

  • 在第2步中找到具備相同簽名的方法
  • 不然在超類中搜索等。

10 Java中的一些重要的Object方法

equals() - 若是兩個對象「相等」,則爲true
hashCode() - 用於哈希映射的哈希代碼
toString() - 可打印的字符串表示形式

toString() - 醜陋而無信息

  • 你知道你的目標是什麼,因此你能夠作得更好
  • 老是覆蓋,除非你知道不會被調用

equals&hashCode - 身份語義

  • 若是你想要價值語義,你必須重寫
  • 不然不要

設計好的類

不可變類的優勢

簡單
本質上是線程安全的
能夠自由分享
不須要防護式拷貝
優秀的積木

如何編寫一個不可變的類

不要提供任何變值器
確保沒有方法可能被覆蓋
使全部的領域最終
使全部字段保密
確保任何可變組件的安全性(避免重複曝光)
實現toString(),hashCode(),clone(),equals()等。

什麼時候讓類不可變
老是,除非有充分的理由不這樣作
老是讓小型「價值類」永恆不變!

什麼時候讓類可變

類表示狀態改變的實體

  • 真實世界 - 銀行帳戶,紅綠燈
  • 抽象 - 迭代器,匹配器,集合
  • 進程類 - 線程,計時器

若是類必須是可變的,則最小化可變性

  • 構造函數應該徹底初始化實例
  • 避免從新初始化方法

OOP的歷史

仿真和麪向對象編程的起源

20世紀60年代:Simula 67是第一個由Kristin Nygaardand Ole-Johan Dahl在挪威計算中心開發的面嚮對象語言,用於支持離散事件模擬。 (類,對象,繼承等)
「面向對象編程(OOP)」這個術語最先是由施樂PARC在他們的Smalltalk語言中使用的。
20世紀80年代:面向對象已經變得很是突出,而其中的主要因素是C ++。
NiklausWirth用於Oberon和Modula-2的模塊化編程和數據抽象;
埃菲爾和Java

總結

面向對象的標準
基本概念:對象,類,屬性,方法和接口
OOP的獨特功能

封裝和信息隱藏
繼承和重寫
多態性,子類型和重載
靜態和動態調度

Java中一些重要的Object方法編寫一個不可變的類OOP的歷史

相關文章
相關標籤/搜索