1. 程式人生 > >1.2 《演算法》之資料抽象

1.2 《演算法》之資料抽象

文章目錄

資料抽象

  • 資料型別即一組值和一組對這些值操作的集合
  • 定義和使用資料型別來抽象一物件,該過程稱為資料抽象(函式抽象風格的補充)
  • Java程式設計基礎主要是使用class關鍵字構造引用資料型別,該程式設計風格稱為面向物件程式設計(核心概念是物件,即儲存了某個資料型別的值的實體)
  • 抽象資料型別是一種能對使用者隱蔽資料表示的資料型別,用Java類實現抽象資料型別類比用靜態方法實現函式庫。抽象資料型別將資料和函式的實現關聯,並將資料的表示方式隱藏起來。
  • 使用抽象資料型別時主要關注API的描述的操作上而不關心資料的表示,而在實現抽象資料型別時,關注資料本身及實現對其操作
  • 本書通過抽象資料型別用API的實現描述演算法和資料結構且以適用於多用途的API準確定義問題

使用抽象資料型別

  • 要使用一種資料型別,不需要知道其內部實現細節
  • 抽象資料型別的基礎是一組值的集合,API(方法)定義操作而不是說明這些值的意義
  • 使用API說明類的行為,繼承的方法在API中顯示為灰色(便於discern)
  • 抽象資料型別靜態方法庫的區別(見書P39,共六點)

繼承

  • ToString()預設返回字串表示的該資料型別值的記憶體地址,當將任意資料型別值與字串值連線時呼叫該方法,一般過載該方法
  • 將程式組織為獨立模組的機制可應用於所有Java類

物件

  • 物件是能夠承載資料型別的值的實體,物件有三大特性:狀態,標識和行為
  • 物件的狀態即資料型別中的值,標識用於區分不同物件(認為標識是物件在記憶體中位置),行為即操作方法
  • 實現一資料型別的唯一職責是維護一個物件的身份,使用該資料型別時只需遵守描述物件行為的API,而不用關注物件狀態的表示方法。
  • 引用是訪問物件的方式,不同的Java實現中引用的實現細節也不同,但可以認為引用就是記憶體地址

建立物件

  • 每種資料型別的值儲存在物件中
  • 建構函式無返回值,因為其總是返回其對應資料型別物件的引用,呼叫建構函式建立物件
  • 呼叫new,則(1)系統為物件分配記憶體空間(2)初始化物件中值(3)返回物件引用
  • 原始資料型別:變數直接與值相關聯,而對於引用資料型別,變數與指向物件的引用關聯
  • 引用資料型別用例隱藏了物件值的表示細節(private),所以不能編寫依賴於任何特定表示方法的程式碼
  • 靜態方法是實現函式,而例項方法是實現對資料型別的操作(對比,見P41的表1.2.2)

使用引用資料型別物件

  • 從引用角度而非值的角度去考慮問題,包括:
    1. 賦值:使用引用型別的賦值語句,建立該引用的一個副本,不建立新物件,只是建立另一個指向該物件的引用(稱為別名),即複製的是引用的值(參照上文的關聯解釋
    2. 傳遞引用型別物件:如引數為counter物件,本質上傳遞的是一個名稱和一個計數器,實際只需要指定一個引用型別變數,因為呼叫一帶參方法的動作效果相當於每個實參在賦值語句右側,而形參在左側,這樣可理解為上述賦值語句的行為,Java將引數值的一個副本從呼叫端傳遞給了方法端(稱為值傳遞機制),對於primitive,複製了變數的實際值,方法無法改變呼叫端的值,而對於引用資料型別,複製了引用的值,可改變傳入的物件的值
    3. 返回引用型別物件
      方法可以返回引數物件(作為形參的物件),彌補了返回值只能為一的缺陷
    4. 建立並使用該型別物件的陣列
      非primitive的值都是物件,即陣列也是引用資料型別,所以傳入或返回的陣列變數都是傳入或返回陣列引用的副本
      引用資料型別的陣列是物件的引用組成的陣列,而不是物件本身組成的

物件小結

  • 運用資料抽象思想(定義和使用資料型別,將資料型別的值封裝在物件中)編寫程式碼的方式稱為面向物件程式設計
  • 資料型別(類)指的是一組值和對值的操作的集合,我們會將資料型別實現在獨立的類模組中並編寫使用示例
  • 物件是能儲存任意該資料型別的值的實體,或類的例項
  • 物件有三大特性:狀態,標識和行為
  • 呼叫new,則(1)系統為物件分配記憶體空間(2)初始化物件中值(3)返回物件引用
  • 原始資料型別:變數直接與值相關聯,而對於引用資料型別,變數與指向物件的引用關聯
  • 引用資料型別的陣列是物件的引用組成的陣列,而不是物件本身組成的

抽象資料型別舉例

  • 編寫Java程式:實現某種抽象資料型別或靜態方法庫
  • 開發,組織–》輕易使用
  • 通過描述性字首區分不同抽象資料型別的不同實現,從整體上來說,抽象資料型別說明組織並理解資料結構是程式設計中的重要成分。
  1. Java lang包中的標準系統抽象資料型別,可被任意Java程式呼叫
  2. Java標準庫中的類,與第1項相比但需要import語句
  3. I/O處理類:處理多個輸入輸出流
  4. 面向資料類的抽象資料型別:主要作用是通過封裝資料的表示來簡化資料的組織和處理,如平面點
  5. 集合類抽象資料型別,簡化對同一型別一組資料的操作
  6. 面向操作的抽象資料型別,用於分析演算法,如計數器
  7. 圖演算法相關的抽象資料型別,包括用來封裝各種圖表示的面向資料類的抽象資料型別,和提供圖的處理演算法的面向操作的抽象資料型別

幾何物件

  • 為基本幾何物件定義抽象資料型別,如點,矩形等

資訊處理

  • 應用的核心是組織和處理資訊,抽象資料型別是組織資訊的一種自然方式
  • 雖然未給出細節,兩份API展示了商業應用程式的一種典型做法
  • 資訊處理程式的典型做法:定義和真實世界中物體相對應的物件
  • 好處在於用例不需要知道資料的表示方法(封裝資料),不去深究組織資訊的方式,只要注意這種做法
  • 用抽象資料型別方式組織資料能將一個物件和其相關資料變成一個整體(如將日期的年月日屬性整合),然後維護一個date物件
  • 實現從Object類繼承下的某些方法能使我們演算法處理任意型別的資料,如在資料結構中包含ToString方法的重寫來列印一由物件值組成的一個字串(習慣用法)
  • 遇到邏輯相關的不同型別的資料,考慮定義抽象資料型別來組織資料(資料抽象),簡化程式碼

字串

  • 一個string值是一串可用索引訪問的char值
  • 可使用字串字面量來建立和初始化一個字串
  • 不用字元陣列取代字串:為了程式碼簡潔清晰,無需關心字串實現方式
  • split("\s+")表示一個或多個製表符,空格,換行符或回車
  • 關注典型的字串處理程式碼(待補P50)

再談輸入輸出(處理多輸入輸出流)

  • StdIn等標準庫缺點在於只能處理單個輸入流,輸出流,通過面向物件程式設計,定義類似機制來實現多輸入輸出流的處理
  • 本書標準庫定義了資料型別In/Out/Draw:帶參的表示來源為檔案或網站,空參的表示來源為標準輸入

抽象資料型別的實現

  • 和靜態方法庫一樣,通過class關鍵字實現抽象資料型別
  • 定義資料型別的值的例項變數(宣告例項變數),定義建構函式和例項方法
  • 單元測試用例main函式用於測試

例項變數

  • 宣告例項變數來定義資料型別的值(每個物件的狀態)
  • 通過private隱藏抽象資料型別的資料表示(抽象性的體現)

建構函式

  • 每個類至少包含一個建構函式,用於初始化例項變數
  • 預設建構函式為空參型別,且各例項變數預設初始化
  • 使用new關鍵字-》觸發建構函式

例項方法(物件的行為)

  • 類比實現靜態方法的程式碼
  • 簽名:指定了方法名,所有引數變數的型別和名稱
  • 例項方法的特性基本都和靜態方法相同,除了它可以訪問並操作例項變數
  • 在例項方法中對例項變數的引用時呼叫該方法的物件的例項變數
  • 呼叫例項方法改變例項變數,與呼叫靜態方法僅僅是語法上的區別,但顛覆思維方式

作用域

例項方法中共包含如下三種變數:

  • 引數變數:作用域為整個方法
  • 區域性變數:宣告和初始化都在方法體內,作用域為定義後的方法體內
  • 例項變數:為該類的物件儲存了資料型別的值,作用域為整個類,如有歧義,用this字首來區分例項變數

API及用例和實現

  • API及用例和實現是實現和使用抽象資料型別所需要理解的基本部分
  • 用例通常獨立成為含有main方法的類,並將main方法預留為一個用於開發和最小單元測試的用例
  • 三步走:1.定義API 2.用一個Java類實現API的定義 3.建立測試用例驗證

抽象資料型別的例項

日期

  • 定義了兩種日期類的實現,都滿足API中的定義(API不指定對實現的要求)
  • 兩種實現各有優缺點,體現在對時間和空間的利用上

維護多個實現

採用如下命名約定

  • 字首的描述性修飾符:BasicDate,SmartDate
  • 維護一個無字首的參考實現

累加器

  • 累加器API定義了一種能計算一組資料實時平均值的抽象資料型別‘
  • 該實現未儲存所有資料的值,避免用光記憶體,所以可應用處理大規模資料(即使在無法全部儲存資料的裝置上)
  • 視覺化的累加器:其實現繼承了累加器類,添加了視覺化的例項方法
  • 仔細而完整地設計API
  • 不願改動API(影響用例程式碼),則可新增一個新的建構函式來取得某些功能(保證原方法還能呼叫)

資料型別的設計

  • 抽象資料型別是一種向用例隱藏內部表示的資料型別。該思想強有力地影響了現代程式設計
  • 例子-》研究抽象資料型別的高階特性和Java實現打下了基礎

封裝性

  • 利用資料型別的實現來封裝資料,簡化實現和隔離用例開發,封裝實現了模組化程式設計,其允許我們
    1. 支援尚未編寫的程式(API起指南作用)
    2. 隔離了對資料型別的值的操作(可在實現中新增一致性檢查等除錯工具)
  • 大型程式分解為獨立開發和除錯的小型模組,各模組獨立,API作為用例和實現之間唯一的依賴

設計API

  • 按照能複用的方式編寫程式
  • 說明書問題(判斷實現與API相符與否):一份說明書應該用一種類似於程式語言的形式語言編寫,而從數學上可證明,判定這樣兩個程式進行的計算是否相同是不可能的
  • 為了驗證設計,在API附近的正文中給出用例程式碼
  • 設計API陷阱,如太粗略(無法提供有效的抽象)或太詳細(抽象過於細緻或發散)或依賴於某種特定的資料表示(用例程式碼無法從資料表示的細節中分離出來)
  • 總而言之,API只為用例提供它們所需要的

演算法與抽象資料型別

  • 資料抽象天生適合演算法研究,因為其能夠為我們提供一個框架,在框架中能夠準確地說明一個演算法的目的和用法
  • 演算法一般是某個抽象資料型別的一個例項方法的實現
  • 白名單例子很自然地實現為一個抽象資料型別的用例,進行了如下操作:
    1. 由一組給定值構造一個set物件
    2. 判定某值是否存在於集合中
  • 將上述操作封裝在抽象資料型別中,StaticSETofInts是更一般也更有用的符號表抽象資料型別的一種特殊情況,二分法是較為適合用於實現符號表抽象資料型別的一種,同BinarySearch相比,StaticSETofInts確保陣列在rank()方法呼叫前被排序
  • 每個Java程式都是一組靜態方法和一種資料型別的實現的集合,關注例項方法和隱藏資料表示
  • 利用類繼承機制來支援資料抽象

Java繼承機制

介面繼承

  • Java為定義物件之間的關係提供了支援,稱為介面,廣泛使用該機制,如比較和迭代
  • 介面機制又稱為子型別機制
  • 不使用非正式的API,為Date宣告一個介面(在Date實現中引用該介面,編譯器會檢查該實現是否與介面相符
  • 該方式稱為介面繼承,因為實現類繼承的是介面
  • 可以在更多非正式的API中使用介面繼承

實現繼承

  • 子類繼承被廣泛用於編寫可擴充套件的庫,來有效重用程式碼

字串表示的習慣

  • 當連線符的一個運算元是字串時,Java自動將另一個運算元也轉換為字串,這個約定是這種自動轉換的基礎,若該物件的資料型別未實現tostring方法,則呼叫object類的預設實現(返回一個含有該物件記憶體地址的字串),一般為每個類實現並重寫tostring方法(重寫時只需隱式呼叫,即通過“+”,呼叫每個例項變數的tostring方法)

封裝型別

  • Java提供了一些內建的引用資料型別,稱為封裝型別,每個原始資料型別都有一個對應的封裝型別
  • 必要時,Java會自動裝箱和拆箱,如int值+string值,則int型別自動轉換為Integer並呼叫tostring方法

等價性(物件相等性問題)

考慮兩個物件相等與否?

  • ==: 若用相同型別的兩個引用變數進行等價性測試,則等等號表示檢測標識,即引用是否相同
  • 若想檢測資料型別的值(物件的狀態)或自定義規則來判斷等價性,則要過載equals方法(等價性測試方法)
  • 一些標準資料型別(封裝型別及string)和複雜資料型別(file,URL)已重寫了equals方法,可直接使用內建的實現
  • Java規定equals必須是一種等價關係,具有自反性,對稱性,傳遞性,一致性,非空型(P64)
  • equals實現步驟:
    1. 若該物件引用同參數物件引用相同,返回true
    2. 引數為空,根據約定,返回false(避免後續程式碼空引用)
    3. 兩物件的類不同,返回false(使用==來判斷Class型別的物件是否相等,因為同一種類的所有物件的getclass方法一定能返回相同的引用)
    4. 將引數物件型別從object轉換為date(前項測試通過,轉換必然成功)
    5. 若任意例項變數的值不同,返回false

記憶體管理

  • 沒有引用指向某物件會某物件離開作用域後成為孤兒物件
  • 必要時分配記憶體,不必要時釋放記憶體
  • 記憶體管理
    1. 對於primitive資料型別,記憶體分配所需要的所有資訊在編譯階段就能夠獲取,宣告變數時預留記憶體空間,離開作用域釋放記憶體空間
    2. 對於引用資料型別,系統在建立物件時則分配記憶體,但程式執行的動態性決定物件何時成為孤兒物件,並不能準確知道何時釋放一個物件記憶體
    3. 在C++中分配和釋放記憶體由程式設計師操作,而Java自動記憶體管理(這種回收記憶體的方式稱為垃圾回收),記錄孤兒物件並將它們的記憶體釋放在記憶體池中,所以Java採用不允許修改引用的策略,使得其能高效自動垃圾回收

不可變性

  • 不可變資料型別,即建立某型別的物件後其例項變數不能改變,如date,string類(按照約定,不使用子類繼承的程式碼中)
  • final強制保證不可變性,變數為final,則其只能被賦值一次(不然產生編譯時錯誤)
  • 不可變性基於應用場景決定,如要封裝不變的值,以便將其和primitive資料型別一樣用於賦值語句等
  • string是不可變的,而陣列是可變的,如將string傳遞給方法,方法改變不了實參string,而若傳入陣列,則可改變
  • 我們希望string的值不變,而陣列可變,但有時希望使用可變字串(stringbuilder類)和不可變陣列(vector類)
  • 使用不可變型別的程式碼更簡單,因為容易確保在用例中使用它們的變數的狀態前後一致,而要時刻關注可變資料型別的值變化情況
  • 不可變意味著要為每個值建立一個新物件,但開銷能接受
  • final只能保證primitive資料型別的不可變性,對於引用資料型別則不行(只能讓其永遠指向同一個物件,但該物件的值本身還是可變的)
  • 設計資料型別要考慮不變性

契約式設計

  • 程式執行時檢驗程式狀態的機制
    1. 異常:控制不可預見的錯誤
      破壞性事件,如Java系統方法丟擲的異常:下標越界等,最簡單的異常:

      throw new RuntimeException(“Error occurs”) //中斷程式執行,列印錯誤資訊

      提倡 “快速出錯”的常規程式設計practice,即一旦出錯則丟擲異常,更早定位錯誤位置

    2. 斷言:驗證假設
      用於確認為true的布林表示式,若為false,則終止程式並報錯,使用斷言確定程式的正確性和記錄我們意圖,比如判斷陣列索引

      assert index>=0 : “Negative index”

      預設未啟用斷言,使用-ea啟用斷言,程式正常操作時不依賴斷言,因為它們可能會被禁用
      契約式程式設計模型思想:使用斷言使得程式不會被錯誤終止或進入死迴圈,設計資料型別時說明呼叫方法的前提條件,說明方法在返回時必須達到的要求(後置條件)和副作用(方法對物件狀態產生的影響),這些條件都可以用斷言進行測試。

小結

  • 本節所討論的語言機制說明資料型別在設計中所遇到的問題
  • 設計資料型別是主要目標,使得大部分工作在抽象層次完成,且和實際問題匹配。

答疑

  1. 資料抽象:help us write dependable and accurate code
  2. 原始資料型別比引用資料型別執行更快
  3. 可使用私有例項方法在公有方法中共享程式碼
  4. 允許直接訪問例項變數的好處don’t outweigh 對資料的特定表達方式依賴所帶來的壞處
  5. 建立有N個物件的陣列要使用N+1次new關鍵字
  6. printIn(Object):自動呼叫該物件的tostring方法
  7. 指標和引用的辨析:
    指標類比Java的引用,可看作機器地址,C語言中指標是一種primitive資料型別,可通過各種方法操作它,但指標程式設計易出錯,需要精心設計指標類的操作來避免犯錯,Java將此觀點發揮到極致,在Java中建立引用的方法只有new,改變引用的方法也只有賦值語句,即程式設計師對引用進行的操作只有建立和複製,所以Java引用被稱為安全指標(Java能保證每個引用都會指向某種型別的物件,且能找出孤兒物件並將其回收)指標類比Java的引用,可看作機器地址,C語言中指標是一種primitive資料型別,可通過各種方法操作它,但指標程式設計易出錯,需要精心設計指標類的操作來避免犯錯,Java將此觀點發揮到極致,在Java中建立引用的方法只有new,改變引用的方法也只有賦值語句,即程式設計師對引用進行的操作只有建立和複製,所以Java引用被稱為安全指標(Java能保證每個引用都會指向某種型別的物件,且能找出孤兒物件並將其回收)
  8. 哪裡找到Java實現引用和進行垃圾回收的細節?
    Java系統的實現各有不同,例如,實現引用的一種自然方式是使用指標(機器地址),另一種則是控制代碼(指標的指標),前者訪問資料的速度更快,後者能更好地實現垃圾回收
  9. 子類繼承的問題
    阻礙模組化程式設計:首先子類完美依賴於父類(fragile的基類問題),其次子類程式碼可以訪問所有例項變數(子類會誤改父類例項變數)
  10. 如何使一個類不可變?
    保證含有一個可變型別的例項變數的資料型別的不可變性,需要得到一個本地副本(稱為保護性複製),且保證沒有任何例項方法能改變資料的值
  11. 引用null表示不指向任何物件對的字面量,
  12. 所有類都含有一個main靜態方法。此外,涉及多個物件操作,若它們(多個物件)都不是觸發該方法(操作多個物件)的合適物件,則新增一個靜態方法,來簡化程式碼
  13. static表示靜態變數,也稱為全域性變數(作用域為全域性),不和具體物件關聯(類的物件共享)

感言

大致複習了一遍Java基本語法,下章正式學習演算法,啦啦啦啦啦啦啦!