<font size="3">html
本文內容來自MIT_6.031_sp18: Software Construction課程的Readings部分,採用CC BY-SA 4.0協議。java
因爲咱們學校(哈工大)大二軟件構造課程的大部分素材取自此,也是推薦的閱讀材料之一,因而打算作一些翻譯工做,本身學習的同時也能幫到一些懶得看英文的朋友。另外,該課程的閱讀資料中有許多練習題,可是沒有標準答案,所給出的「正確答案」均爲譯者所寫,有錯誤的地方還請指出。程序員
(更新:從第10章開始,只提供正確答案,再也不翻譯錯誤答案)web
<br />數據庫
<br />編程
審校:李秋豪數組
V1.0 Thu Mar 29 00:41:23 CST 2018安全
<br />數據結構
在這篇閱讀中,咱們將會講解一個重要的概念——抽象數據類型,它會幫助咱們將數據結構的使用和數據結構的具體實現分開。
抽象數據類型解決了一個很危險的問題:使用者可能對類型的內部表示作假設。咱們在後面會探討爲何這種假設是危險的,以及如何避免它。咱們也會討論操做符的分類和如何設計好的抽象數據類型。
<br />
閱讀如下代碼並回答問題:
class Wallet { private int amount; public void loanTo(Wallet that) { // put all of this wallet's money into that wallet /*A*/ that.amount += this.amount; /*B*/ amount = 0; } public static void main(String[] args) { /*C*/ Wallet w = new Wallet(); /*D*/ w.amount = 100; /*E*/ w.loanTo(w); } } class Person { private Wallet w; public int getNetWorth() { /*F*/ return w.amount; } public boolean isBroke() { /*G*/ return Wallet.amount == 0; } }
假設程序在運行 /*A*/
語句後當即中止,上圖列出了此時的內部狀態,請問各個數字所標出的方框內應該填上什麼?
1 -> w
2 -> that
3 -> loanTo
4 -> 200
Access control A
關於語句 /*A*/
,如下哪個說法是正確的?
that.amount += this.amount;
[x] 在Java中容許對this.amount
的索引
[x] 在Java中容許對 that.amount
的索引
Access control B
關於語句 /*B*/
,如下哪個說法是正確的?
amount = 0;
amount
的索引Access control C
關於語句 /*C*/
,如下哪個說法是正確的?
Wallet w = new Wallet();
Wallet()
構造函數的調用Access control D
關於語句 /*D*/
,如下哪個說法是正確的?
w.amount = 100;
w.amount
的訪問Access control E
關於語句 /*E*/
,如下哪個說法是正確的?
w.loanTo(w);
loanTo()
的調用w
指向的Wallet
對象的金額將會是0Access control F
關於語句 /*F*/
,如下哪個說法是正確的?
return w.amount;
[x] 這裏關於 w.amount
的索引不會被容許,由於 amount
是在另外一個類中的私有區域
[x] 這個非法訪問會被靜態捕捉
Access control G
關於語句 /*G*/
,如下哪個說法是正確的?
return Wallet.amount == 0;
Wallet.amount
的索引不會被容許,由於 amount
是一個私有地址Wallet.amount
的索引不會被容許,由於 amount
是一個實例變量<br />
抽象數據類型是軟件工程中一個廣泛原則的實例,從它衍生出不少意思相近的名詞。這裏列出了幾個可以表達其中思想的詞:
做爲一個軟件工程師,你應該知道這些名詞,由於你會在之後的工做中常常遇到它們。這些思想的本質目的都是爲了實現咱們這門課的三個目標:遠離bug、易於理解、可改動。
事實上,咱們在以前的課程中已經碰到過這些思想,特別是在設計方法和規格說明的時候:
從今天的課程開始,咱們將跳出對方法的抽象,看看對數據的抽象。可是在咱們描述數據抽象時方法也會扮演很重要的角色。
在早期的編程語言中,用戶只能本身定義方法,而全部的類型都是規定好的(例如整型、布爾型、字符串等等)。而現代編程語言容許用戶本身定義類型對數據進行抽象,這是軟件開發中的一個巨大進步。
對數據進行抽象的核心思想就是類型是經過其對應的操做來區分的:一個整型就是你能對它進行加法和乘法的東西;一個布爾型就是你能對它進行取反的東西;一個字符串就是你能對它進行連接或者取子字符串的東西,等等。在必定意義上,用戶在之前的編程語言上彷佛已經可以定義本身的類型了,例如定義一個名叫Date的結構體,裏面用int表示天數和年份。可是真正使得抽象類型變得新穎不一樣的是對操做的強調:用戶不用管這個類型裏面的數據是怎麼保存表示的,就好像是程序員不用管編譯器是怎麼存儲整數同樣。起做用的只是類型對應的操做。
和不少現代語言同樣,在Java中內置類型和用戶定義類型之間的關係很模糊。例如在 java.lang
中的類 Integer
和 Boolean
就是內置的——Java標準中規定它們必須存在,可是它們的定義又是和用戶定義類型的方式同樣的。另外,Java中還保留了原始類型,它們不是類和對象,例如 int
和 boolean
,用戶沒法對它們進行繼承。
Abstract Data Types
思考抽象數據類型 Bool
,它有以下操做:
true : Bool false : Bool
and : Bool × Bool → Bool or : Bool × Bool → Bool not : Bool → Bool
頭兩個操做構建了這個類型對應的兩個值,後三個操做對應邏輯操做 和、或、取非。
如下哪些選項能夠是 Bool
具體的實現方法(而且知足上面的操做符)?
int
值,5表明true,8表明falseString
對象的索引,"false"
表明true, "true"
表明falseint
值,大於1的質數表明true,其他的表明false<br />
對於類型,無論是內置的仍是用戶定義的,均可以被分爲可改變 和 不可變兩種。其中可改變類型的對象可以被改變:它們提供了改變對象內容的操做,這樣的操做執行後能夠改變其餘對該對象操做的返回值。因此 Date
就是可改變的,由於你能夠經過調用setMonth
操做改變 getMonth
操做的返回值。但 String
就是不可改變的,由於它的操做符都是建立一個新的 String
對象而不是改變現有的這個。有時候一個類型會提供兩種形式,一種是可改變的一種是不可改變的。例如 StringBuilder
就是一種可改變的字符串類型。
而抽象類型的操做符大體分類:
String
類裏面的 concat
方法就是一個生產者,它接受兩個字符串而後據此產生一個新的字符串。List
的 size
方法,它返回一個 int
。List
的 add
方法,它會在列表中添加一個元素。咱們能夠將這種區別用映射來表示:
其中T表明抽象類型自己;t表明其餘的類型;+
表明這個參數可能出現一次或屢次;*
表明這個參數可能出現零次或屢次。例如, String.concat()
這個接受兩個參數的生產者:
有些觀察者不會接受其餘類型的參數,例如:
而有些則會接受不少參數:
構造者一般都是用構造函數實現的,例如 new ArrayList()
,可是有的構造體是靜態方法(類方法),例如 Arrays.asList()
和 String.valueOf
,這樣的靜態方法也稱爲工廠方法。
改造者一般沒有返回值(void
)。一個沒有返回值的方法必定有反作用 ,由於否則這個方法就沒有任何意義了。可是不是全部的改造者都沒有返回值。例如Set.add()
會返回一個布爾值用來提示這個集合是否被改變了。在Java圖形庫接口中,Component.add()
會將它本身這個對象返回,所以add()
能夠被連續鏈式調用。
int 是Java中的原始整數類型,它是不可變類型,沒有改造者。
0
, 1
, 2
, …+
, -
, *
, /
==
, !=
, <
, >
List 是Java中的列表類型,它是可更改類型。另外,List
也是一個接口,因此對於它的實現能夠有不少類,例如 ArrayList
和 LinkedList
.
ArrayList
和 LinkedList
的構造函數, Collections.singletonList
Collections.unmodifiableList
size
, get
add
, remove
, addAll
, Collections.sort
String 是Java中的字符串類型,它是不可變類型。
String
構造函數, valueOf
靜態方法(工廠方法)concat
, substring
, toUpperCase
length
, charAt
這個分類告訴了咱們一些有用的術語,但它不是完美的。例如對於複雜的數據類型,有些操做可能既是生產者也是改造者。
Operations
下面都是咱們從Java庫中選取的幾個抽象數據類型的操做,試着經過閱讀文檔將這些操做分類。
提示:注意類型自己是否是參數或者返回值,同時記住實例方法(沒有static
關鍵詞的)有一個隱式的參數。
creator
producer
mutator
producer
observer
observer
mutator
<br />
這一節的重要思想就是抽象類型是經過它的操做定義的.
對於類型T來講,它的操做集合和規格說明徹底定義和構造了它的特性。例如,當咱們談到List
類型時,咱們並無特指一個數組或者連接鏈表,而是一系列模糊的值——哪些對象能夠是List
類型——知足該類型的規格說明和操做規定,例如 get()
, size()
, 等等。
上一段說到的「模糊的值」是指咱們不能去檢查數據具體是在類型中怎麼存儲的,而是要經過特定的操做去處理。例如上圖中畫出的,經過規格說明這道「防火牆」,咱們將類型中具體的實現和這些實現共享的私有數據封裝起來,而用戶只能看到和使用接口上的操做。
<br />
設計一個抽象類型包括選擇合適的操做以及它們對應的行爲,這裏列出了幾個重要的規則。
設計少許,簡單,能夠組合實現強大功能的操做而非設計不少複雜的操做。
每一個操做都應該有一個被明肯定義的目的,而且應該設計爲對不一樣的數據結構有一致的行爲,而不是針對某些特殊狀況。例如,或許咱們不該該爲List
類型添加一個sum
操做。由於這雖然可能對想要操做一個整數列表的用戶有幫助,可是若是用戶想要操做一個字符串列表呢?或者一個嵌套的列表? 全部這些特殊狀況都將會使得sum
成爲一個難以理解和使用的操做。
操做集合應該充分地考慮到用戶的需求,也就是說,用戶能夠用這個操做集合作他們可能想作的計算。一個較好測試方法是檢查抽象類型的每一個屬性是否都能被操做集提取出來。例如,若是沒有get
操做,咱們就不能提取列表中的元素。抽象類型的基本信息的提取也不該該特別困難。例如,size
方法對於List
並非必須的,由於咱們能夠用get
增序遍歷整個列表,直到get
執行失敗,可是這既不高效,也不方便。
抽象類型能夠是通用的:例如,列表、集合,或者圖。或者它能夠是適用於特定領域的:一個街道的地圖,一個員工數據庫,一個電話簿等等。可是一個抽象類型不能兼有上述兩者的特性。被設計用來表明一個紙牌序列的Deck
類型不該該有一個通用的add
方法來向類型實例中添加任意對象,好比整型和字符串類型。反過來講,對於像dealCards
這樣的只對特定領域(譯者注:紙牌遊戲)有效的方法,把它加入List
這樣的通用類型中也是沒有意義的。
<br />
特別地,一個好的抽象數據類型應該是表示獨立的。這意味着它的使用和它的內部表示(實際的數據結構和實現)無關,因此內部表示的改變將對外部的代碼沒有影響。例如,List
就是表示獨立的——它的使用與它是用數組仍是鏈接鏈表實現無關。
若是一個操做徹底在規格說明中定義了前置條件和後置條件,使用者就知道他應該依賴什麼,而你也能夠安全的對內部實現進行更改(遵循規格說明)。
讓咱們先來看看一個表示獨立的例子,而後想一想它爲何頗有用。下面的 MyString
抽象類型是咱們舉出的例子,雖然它遠遠沒有Java中的String
操做多,規格說明也有些不一樣,可是仍是有解釋力的。下面是規格說明:
/** MyString represents an immutable sequence of characters. */ public class MyString { //////////////////// Example of a creator operation /////////////// /** @param b a boolean value * @return string representation of b, either "true" or "false" */ public static MyString valueOf(boolean b) { ... } //////////////////// Examples of observer operations /////////////// /** @return number of characters in this string */ public int length() { ... } /** @param i character position (requires 0 <= i < string length) * @return character at position i */ public char charAt(int i) { ... } //////////////////// Example of a producer operation /////////////// /** Get the substring between start (inclusive) and end (exclusive). * @param start starting index * @param end ending index. Requires 0 <= start <= end <= string length. * @return string consisting of charAt(start)...charAt(end-1) */ public MyString substring(int start, int end) { ... } }
使用者只須要/只能知道這個類型的公共方法和規格說明。
如今讓咱們看一個MyString
簡單的表示方法,僅僅使用一個字符數組,並且它的大小恰好是字符串的長度,沒有多餘的空間:
private char[] a;
若是使用這種表示方法,咱們對操做的實現可能就是這樣的:
public static MyString valueOf(boolean b) { MyString s = new MyString(); s.a = b ? new char[] { 't', 'r', 'u', 'e' } : new char[] { 'f', 'a', 'l', 's', 'e' }; return s; } public int length() { return a.length; } public char charAt(int i) { return a[i]; } public MyString substring(int start, int end) { MyString that = new MyString(); that.a = new char[end - start]; System.arraycopy(this.a, start, that.a, 0, end - start); return that; }
這裏想一個問題:爲何 charAt
和 substring
不去檢查參量在合法的範圍內?你認爲這種類型的對象對於非法的輸入會有什麼反應?
下面的快照圖展現了在使用者進行substring
操做後的數據狀態:
MyString s = MyString.valueOf(true); MyString t = s.substring(1,3);
這種實現有一個性能上的問題,由於這個數據類型是不可變的,那麼 substring
實際上沒有必要真正去複製子字符串到一個新的數組中。它能夠僅僅指向原來的 MyString
字符數組,而且記錄當前的起始位置和終止位置。
爲了實現這種優化,咱們能夠將內部表示改成:
private char[] a; private int start; private int end;
經過這種新的表示方法,咱們能夠這樣實現操做:
public static MyString valueOf(boolean b) { MyString s = new MyString(); s.a = b ? new char[] { 't', 'r', 'u', 'e' } : new char[] { 'f', 'a', 'l', 's', 'e' }; s.start = 0; s.end = s.a.length; return s; } public int length() { return end - start; } public char charAt(int i) { return a[start + i]; } public MyString substring(int start, int end) { MyString that = new MyString(); that.a = this.a; that.start = this.start + start; that.end = this.start + end; return that; }
如今進行substring
操做後的數據狀態:
MyString s = MyString.valueOf(true); MyString t = s.substring(1,3);
由於 MyString
的使用者只使用到了它的公共方法和規格說明(沒有使用私有的存儲表示),咱們能夠「私底下」完成這種優化而不用擔憂影響使用者的代碼。這就是表示獨立的力量。
Representation 1
思考下面這個抽象類型:
/** * Represents a family that lives in a household together. * A family always has at least one person in it. * Families are mutable. */ class Family { // the people in the family, sorted from oldest to youngest, with no duplicates. public List<Person> people; /** * @return a list containing all the members of the family, with no duplicates. */ public List<Person> getMembers() { return people; } }
下面是一個使用者的代碼:
void client1(Family f) { // get youngest person in the family Person baby = f.people.get(f.people.size()-1); ... }
假設全部的代碼都能順利運行( Family
和 client1
)並經過測試。
如今 Family
的數據表示從 List
變爲了 Set
:
/** * Represents a family that lives in a household together. * A family always has at least one person in it. * Families are mutable. */ class Family { // the people in the family public Set<Person> people; /** * @return a list containing all the members of the family, with no duplicates. */ public List<Person> getMembers() { return new ArrayList<>(people); } }
如下哪個選項是在 Family
更改後對 client1
的影響?
client1
依賴於 Family
的數據表示, 而且這種依賴會致使靜態錯誤。Representation 2
原始版本:
/** * Represents a family that lives in a * household together. A family always * has at least one person in it. * Families are mutable. */ class Family { // the people in the family, // sorted from oldest to youngest, // with no duplicates. public List<Person> people; /** @return a list containing all * the members of the family, * with no duplicates. */ public List<Person> getMembers() { return people; } }
新版本:
/** * Represents a family that lives in a * household together. A family always * has at least one person in it. * Families are mutable. */ class Family { // the people in the family public Set<Person> people; /** * @return a list containing all * the members of the family, * with no duplicates. */ public List<Person> getMembers() { return new ArrayList<>(people); } }
使用者 client2
的代碼:
void client2(Family f) { // get size of the family int familySize = f.people.size(); ... }
如下哪個選項是新版本對 client2
的影響?
client2
依賴於 Family
的表示,這種依賴不會被捕捉錯誤可是會(幸運地)獲得正確答案。Representation 3
原始版本:
/** * Represents a family that lives in a * household together. A family always * has at least one person in it. * Families are mutable. */ class Family { // the people in the family, // sorted from oldest to youngest, // with no duplicates. public List<Person> people; /** @return a list containing all * the members of the family, * with no duplicates. */ public List<Person> getMembers() { return people; } }
新版本:
/** * Represents a family that lives in a * household together. A family always * has at least one person in it. * Families are mutable. */ class Family { // the people in the family public Set<Person> people; /** * @return a list containing all * the members of the family, * with no duplicates. */ public List<Person> getMembers() { return new ArrayList<>(people); } }
使用者 client3
的代碼:
void client3(Family f) { // get any person in the family Person anybody = f.getMembers().get(0); ... }
如下哪個選項是新版本對 client3
的影響?
client3
獨立於 Family
的數據表示, 因此它依然能正確的工做Representation 4
對於上面的Family
數據類型,對每行/段判斷他是規格說明(specification)仍是數據表示(representation)仍是具體實現(implementation)?
/** * Represents a family that lives in a household together. * A family always has at least one person in it. * Families are mutable. */
--> 規格說明
public class Family {
--> 規格說明
// the people in the family, sorted from oldest to youngest, with no duplicates.
--> 數據表示
private List<Person> people;
--> 數據表示
/** * @return a list containing all the members of the family, with no duplicates. */
--> 規格說明
public List<Person> getMembers() {
--> 規格說明
return people;
--> 具體實現
<br />
讓咱們總結一下咱們在這篇文章中討論過的主要思想以及使用JAVA語言特性實現它們的具體方法,這些思想對於使用任何語言編程通常都是適用的。重點在於有不少種方式來實現,很重要的一點是:既要對大概念(好比構造操做:creator operation)有較好的理解,也要理解它們不一樣的實現方式。
ADT concept | Ways to do it in Java | Examples |
---|---|---|
Abstract data type | Class | String |
Interface + class(es) | List and ArrayList |
|
Enum | DayOfWeek |
|
Creator operation | Constructor | ArrayList() |
Static (factory) method | Collections.singletonList() , Arrays.asList() |
|
Constant | BigInteger.ZERO |
|
Observer operation | Instance method | List.get() |
Instance method | Collections.max() |
|
Producer operation | Instance method | String.trim() |
Static method | Collections.unmodifiableList() |
|
Mutator operation | Instance method | List.add() |
Static method | Collections.copy() |
|
Representation | private fields |
這個表中有三項咱們尚未在以前的閱讀中講過:
List
和 ArrayList
這些例子,而且咱們將會在之後的閱讀中討論接口。enum
)定義一個抽象數據類型。枚舉對於有固定取值集合的ADTs(例如一週中有周1、週二等等)來講,是很理想的類型。咱們將會在之後的閱讀中討論枚舉。<br />
當咱們測試一個抽象數據類型的時候,咱們分別測試它的各個操做。而這些測試不可避免的要互相交互:咱們只能經過觀察者來判斷其餘的操做的測試是否成功,而測試觀察者的惟一方法是建立對象而後使用觀察者。
下面是咱們測試 MyString
類型時對輸入空間的一種可能劃分方案:
// testing strategy for each operation of MyString: // // valueOf(): // true, false // length(): // string len = 0, 1, n // string = produced by valueOf(), produced by substring() // charAt(): // string len = 1, n // i = 0, middle, len-1 // string = produced by valueOf(), produced by substring() // substring(): // string len = 0, 1, n // start = 0, middle, len // end = 0, middle, len // end-start = 0, n // string = produced by valueOf(), produced by substring()
如今咱們試着用測試用例覆蓋每個分區。注意到 assertEquals
並不能直接應用於 MyString
對象,由於咱們沒有在 MyString
上定義判斷相等的操做,因此咱們只能使用以前定義的 valueOf
, length
, charAt
, 以及 substring
,例如:
@Test public void testValueOfTrue() { MyString s = MyString.valueOf(true); assertEquals(4, s.length()); assertEquals('t', s.charAt(0)); assertEquals('r', s.charAt(1)); assertEquals('u', s.charAt(2)); assertEquals('e', s.charAt(3)); } @Test public void testValueOfFalse() { MyString s = MyString.valueOf(false); assertEquals(5, s.length()); assertEquals('f', s.charAt(0)); assertEquals('a', s.charAt(1)); assertEquals('l', s.charAt(2)); assertEquals('s', s.charAt(3)); assertEquals('e', s.charAt(4)); } @Test public void testEndSubstring() { MyString s = MyString.valueOf(true).substring(2, 4); assertEquals(2, s.length()); assertEquals('u', s.charAt(0)); assertEquals('e', s.charAt(1)); } @Test public void testMiddleSubstring() { MyString s = MyString.valueOf(false).substring(1, 2); assertEquals(1, s.length()); assertEquals('a', s.charAt(0)); } @Test public void testSubstringIsWholeString() { MyString s = MyString.valueOf(false).substring(0, 5); assertEquals(5, s.length()); assertEquals('f', s.charAt(0)); assertEquals('a', s.charAt(1)); assertEquals('l', s.charAt(2)); assertEquals('s', s.charAt(3)); assertEquals('e', s.charAt(4)); } @Test public void testSubstringOfEmptySubstring() { MyString s = MyString.valueOf(false).substring(1, 1).substring(0, 0); assertEquals(0, s.length()); }
Partition covering
哪個測試覆蓋了分區「charAt()
以及字符串長度=1」?
testMiddleSubstring
哪個測試覆蓋了分區「子字符串的子字符串」?
testSubstringOfEmptySubstring
哪個測試覆蓋了分區「valueOf(true)
」?
[x] testValueOfTrue
[x] testEndSubstring
Unit testing an ADT
testValueOfTrue
測試的是哪個「單元」?
valueOf
操做length
操做charAt
操做<br />
T將本次閱讀的內容和咱們的三個目標聯繫起來:
</font>