日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

麻省理工18年春软件构造课程阅读06“规格说明”

發(fā)布時(shí)間:2023/12/10 编程问答 23 豆豆
生活随笔 收集整理的這篇文章主要介紹了 麻省理工18年春软件构造课程阅读06“规格说明” 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

本文內(nèi)容來(lái)自MIT_6.031_sp18: Software Construction課程的Readings部分,采用CC BY-SA 4.0協(xié)議。

由于我們學(xué)校(哈工大)大二軟件構(gòu)造課程的大部分素材取自此,也是推薦的閱讀材料之一,于是打算做一些翻譯工作,自己學(xué)習(xí)的同時(shí)也能幫到一些懶得看英文的朋友。另外,該課程的閱讀資料中有許多練習(xí)題,但是沒(méi)有標(biāo)準(zhǔn)答案,所給出的答案均為譯者所寫(xiě),有錯(cuò)誤的地方還請(qǐng)指出。




譯者:李秋豪

審校:

V1.0 Tue Mar 13 22:17:35 CST 2018


本次課程的目標(biāo)

  • 理解方法規(guī)格說(shuō)明中的前置條件和后置條件,并能夠?qū)懗稣_的規(guī)格說(shuō)明
  • B能夠針對(duì)規(guī)格說(shuō)明寫(xiě)出測(cè)試
  • 理解Java中的檢查異常和非檢查異常(checked and unchecked exceptions)
  • 理解如何用異常處理特殊的結(jié)果

概要

規(guī)格說(shuō)明是團(tuán)隊(duì)合作中的關(guān)鍵點(diǎn)。如果沒(méi)有規(guī)格說(shuō)明,就沒(méi)有辦法分工實(shí)現(xiàn)各種方法。規(guī)格說(shuō)明就像一份合同:實(shí)現(xiàn)者的義務(wù)在于滿(mǎn)足合同的要求,客戶(hù)可以依賴(lài)這些要求工作。事實(shí)上,我們會(huì)發(fā)現(xiàn)就像真的合同一樣,規(guī)格說(shuō)明對(duì)雙方都有制約:當(dāng)合同上有前置條件時(shí),客戶(hù)有責(zé)任滿(mǎn)足這些條件。

在這篇閱讀材料中我們會(huì)研究方法中的規(guī)格說(shuō)明,討論前置條件和后置條件分別是什么,它們對(duì)方法的實(shí)現(xiàn)者和使用者來(lái)說(shuō)意味著什么。我們也會(huì)討論如何使用異常——Java、Python、以及很多現(xiàn)代語(yǔ)言中的一個(gè)重要特性,它使得方法的接口更加安全也更加易懂。


為什么要使用規(guī)格說(shuō)明

在編程中,很多讓人抓狂的bug是由于兩個(gè)地方的代碼對(duì)于接口行為的理解不一樣。雖然每一個(gè)程序員在心里都有一份“規(guī)格說(shuō)明”,但是不是所有程序員都會(huì)把他們寫(xiě)下來(lái)。最終,一個(gè)團(tuán)隊(duì)中的不同程序員對(duì)于同一個(gè)接口就有不同的“規(guī)格說(shuō)明”了。當(dāng)程序崩潰的時(shí)候,就很難發(fā)現(xiàn)問(wèn)題在哪里。簡(jiǎn)潔準(zhǔn)確的的規(guī)格說(shuō)明使得我們遠(yuǎn)離bug,更可以快速發(fā)現(xiàn)問(wèn)題所在。

規(guī)格說(shuō)明對(duì)使用者(客戶(hù))來(lái)說(shuō)也是很有用的,它們使得使用者不必去閱讀源碼。如果你還不相信閱讀規(guī)格說(shuō)明比閱讀源碼更簡(jiǎn)單易懂的話(huà),看看下面這個(gè)標(biāo)準(zhǔn)的Java規(guī)格說(shuō)明和它對(duì)應(yīng)的源碼,它是 BigInteger 中的一個(gè)方法:

API 文檔中的規(guī)格說(shuō)明:

public BigInteger add(BigInteger val)Returns a BigInteger whose value is (this + val).Parameters: val - value to be added to this BigInteger.Returns: this + val

Java 8 中對(duì)應(yīng)的源碼:

if (val.signum == 0)return this; if (signum == 0)return val; if (val.signum == signum)return new BigInteger(add(mag, val.mag), signum);int cmp = compareMagnitude(val); if (cmp == 0)return ZERO; int[] resultMag = (cmp > 0 ? subtract(mag, val.mag): subtract(val.mag, mag)); resultMag = trustedStripLeadingZeroInts(resultMag);return new BigInteger(resultMag, cmp == signum ? 1 : -1);

可以看到,通過(guò)閱讀 BigInteger.add 的規(guī)格說(shuō)明,客戶(hù)可以直接了解如何使用 BigInteger.add ,以及它的行為屬性。如果我們?nèi)ラ喿x源碼,我們就不得不看 BigInteger 的構(gòu)造體, compare-Magnitude, subtract以及trusted-StripLeadingZero-Ints 的實(shí)現(xiàn)——而這還僅僅只是開(kāi)始。

另外,規(guī)格說(shuō)明對(duì)于實(shí)現(xiàn)者也是很有好處的,因?yàn)樗鼈兘o了實(shí)現(xiàn)者更改實(shí)現(xiàn)策略而不告訴使用者的自由。同時(shí),規(guī)格說(shuō)明可以限定一些特殊的輸入,這樣實(shí)現(xiàn)者就可以省略一些麻煩的檢查和處理,代碼也可以運(yùn)行的更快。

如上圖所示,規(guī)格說(shuō)明就好像一道防火墻一樣將客戶(hù)和實(shí)現(xiàn)者隔離開(kāi)。它使得客戶(hù)不必知道這個(gè)單元是如何運(yùn)行的(不必閱讀源碼),也使得實(shí)現(xiàn)者不必管這個(gè)單元會(huì)被怎么使用(因?yàn)榭蛻?hù)要遵守前置條件)。這種隔離造成了“解耦”(decoupling),客戶(hù)自己的代碼和實(shí)現(xiàn)者的代碼可以獨(dú)立發(fā)生改動(dòng),只要雙方都遵循規(guī)格說(shuō)明對(duì)應(yīng)的制約。


行為等價(jià)

思考下面兩個(gè)方法的異同:

static int findFirst(int[] arr, int val) {for (int i = 0; i < arr.length; i++) {if (arr[i] == val) return i;}return arr.length; }static int findLast(int[] arr, int val) {for (int i = arr.length -1 ; i >= 0; i--) {if (arr[i] == val) return i;}return -1; }

當(dāng)然,這兩個(gè)方法的代碼是不同的,名字的含義也不一樣。為了判斷“行為等價(jià)”,我們必須判斷一個(gè)方法是否可以替換另一個(gè)方法,而程序的行為不發(fā)生改變。

除了代碼,它們的行為也不一樣:

  • 當(dāng)val找不到時(shí),fingFirst返回arr的長(zhǎng)度而findLast返回-1;
  • 當(dāng)數(shù)組中有兩個(gè)val的時(shí)候,findFirst返回較小的那個(gè)索引,而findLast返回較大的那個(gè)。

但是當(dāng)val在數(shù)組中僅有一個(gè)的時(shí)候,這兩個(gè)方法的行為是一樣的。也只有在這種情況下,我們才可以將方法的實(shí)現(xiàn)在兩者中互換。

“行為等價(jià)”是對(duì)于“旁觀者”來(lái)說(shuō)的——就是客戶(hù)。為了讓實(shí)現(xiàn)方法可以發(fā)生改動(dòng),我們就需要一個(gè)規(guī)格說(shuō)明要求客戶(hù)遵守某一些制約/前置條件。

所以,我們的規(guī)格說(shuō)明可能是這樣的:

static int find(int[] arr, int val) - requires:val occurs exactly once in arr - effects:returns index i such that arr[i] = val

閱讀小練習(xí)

Behave nicely

static int findFirst(int[] a, int val) {for (int i = 0; i < a.length; i++) {if (a[i] == val) return i;}return a.length; } static int findLast(int[] a, int val) {for (int i = a.length - 1 ; i >= 0; i--) {if (a[i] == val) return i;}return -1; }

假設(shè)客戶(hù)只關(guān)心val是否在a中出現(xiàn)了一次。在這種情況下,findFirst 和 findLast 的行為等價(jià)嗎?

Yes

Best behavior

現(xiàn)在來(lái)改變一下規(guī)格說(shuō)明,假設(shè)客戶(hù)對(duì)返回值要求:

  • 如果val在a中,返回任何索引i ,使得a[i] == val 。
  • 否則,返回一個(gè)不在a索引范圍內(nèi)的整數(shù)j

在這種情況下,findFirst 和 findLast 的行為等價(jià)嗎?

Yes


規(guī)格說(shuō)明的結(jié)構(gòu)

一個(gè)規(guī)格說(shuō)明含有以下兩個(gè)“條款”:

  • 一個(gè)前置條件,關(guān)鍵詞是requires
  • 一個(gè)后置條件,關(guān)鍵詞是effects

其中前置條件是客戶(hù)的義務(wù)(誰(shuí)調(diào)用的這個(gè)方法)。它確保了方法被調(diào)用時(shí)所處的狀態(tài)。

而后置條件是實(shí)現(xiàn)者的義務(wù)。如果前置條件得到了滿(mǎn)足,那么該方法的行為應(yīng)該符合后置條件的要求,例如返回一個(gè)合適的值,拋出一個(gè)特定的異常,修改一個(gè)特定的對(duì)象等等。

如果前置條件不滿(mǎn)足的話(huà),實(shí)現(xiàn)也不需要滿(mǎn)足后置條件——方法可以做任何事情,例如不終止而是拋出一個(gè)異常、返回一個(gè)任意的值、做一個(gè)任意的修改等等。

閱讀小練習(xí)

Logical implication

思考下面這個(gè)規(guī)格說(shuō)明

static int find(int[] arr, int val) - requires:val occurs exactly once in arr - effects:returns index i such that arr[i] = val

作為find的實(shí)現(xiàn)者,下面哪些行為是合法的?

  • [x] 如果arr為空,返回0

  • [x] 如果arr為空,拋出一個(gè)異常

  • [x] 如果val在arr出現(xiàn)了兩次,拋出一個(gè)異常

  • [x] 如果val在arr出現(xiàn)了兩次,將arr中的元素都設(shè)置為0,然后拋出一個(gè)異常

  • [x] 如果arr不為空但是val沒(méi)有出現(xiàn),選取一個(gè)隨機(jī)的索引,將其對(duì)應(yīng)的元素設(shè)置為val ,然后返回這個(gè)索引

  • [x] 如果arr[0]是val ,繼續(xù)檢查剩下的元素,返回索引最高的那個(gè)val對(duì)飲的索引(沒(méi)有再次找到val就返回0)

Logical implementation

作為find的實(shí)現(xiàn)者,當(dāng)arr為空的時(shí)候,為什么要拋出一個(gè)異常?

  • [ ] DRY(譯者注:Don't repeat yourself)
  • [x] 快速失敗/報(bào)錯(cuò)
  • [ ] 避免幻數(shù)
  • [ ] 一個(gè)變量只有一個(gè)目的
  • [ ] 避免全局變量
  • [ ] 返回結(jié)果

Java中的規(guī)格說(shuō)明

有一些語(yǔ)言(例如 Eiffel ),將前置條件和后置條件作為語(yǔ)言的基礎(chǔ)之一,以便程序運(yùn)行的時(shí)候(或者編譯器)可以自動(dòng)檢查客戶(hù)和實(shí)現(xiàn)者是否都遵循了規(guī)格說(shuō)明。

Java并沒(méi)有這么嚴(yán)格,但是它的靜態(tài)檢查也是屬于一種前置條件和后置條件的檢查(編譯器)。至于剩下的部分——那些不屬于數(shù)據(jù)類(lèi)型范疇的約束——必須通過(guò)注釋寫(xiě)在方法的前面,通過(guò)人們來(lái)檢查和保證。

Java對(duì)于 文檔注釋有一些傳統(tǒng),例如參數(shù)的說(shuō)明以 @param作為開(kāi)頭,返回的說(shuō)明以@return 作為開(kāi)頭。你應(yīng)該將前置條件放在@param 的地方,后置條件放在 @return的地方。例如,一個(gè)規(guī)格說(shuō)明可能是這樣:

static int find(int[] arr, int val) - requires:val occurs exactly once in arr - effects:returns index i such that arr[i] = val

… 它在Java中可能被注釋為這樣:

/*** Find a value in an array.* @param arr array to search, requires that val occurs exactly once* in arr* @param val value to search for* @return index i such that arr[i] = val*/ static int find(int[] arr, int val)

Java API 文檔 就是通過(guò)Java標(biāo)準(zhǔn)庫(kù)源碼中的規(guī)格說(shuō)明注釋生成的. 同樣的,Eclipse也可以根據(jù)你的規(guī)格說(shuō)明產(chǎn)生對(duì)應(yīng)的文檔),或者產(chǎn)生和Java API一個(gè)格式的 HTML 文檔 ,這對(duì)你和你的客戶(hù)來(lái)說(shuō)都是很有用的信息。

參考閱讀:

Java: Javadoc Comments

Oracle: How to Write Doc Comments

閱讀小練習(xí)

Javadoc

思考以下規(guī)格說(shuō)明:

static boolean isPalindrome(String word) - requires:word contains only alphanumeric characters - effects:returns true if and only if word is a palindrome

對(duì)應(yīng)的Javadoc注釋:

/** Check if a word is a palindrome.* A palindrome is a sequence of characters* that reads the same forwards and backwards.* @param String word* @requires word contains only alphanumeric characters* @effects returns true if and only if word is a palindrome* @return boolean*/

請(qǐng)問(wèn)Javadoc中哪一行是有問(wèn)題的?

  • [x] /*
  • [ ] * Check if a word is a palindrome.
  • [ ] * A palindrome is a sequence of characters
  • [ ] * that reads the same forwards and backwards.
  • [x] * @param String word
  • [x] * @requires word contains only alphanumeric characters
  • [x] * @effects returns true if and only if word is a palindrome
  • [x] * @return boolean
  • [ ] */

Concise Javadoc specs

思考下面這個(gè)規(guī)格說(shuō)明Javadoc,判斷每一句的作用(逆序):

/*** Calculate the potential energy of a mass in Earth's gravitational field.* @param altitude altitude in meters relative to sea level* @return potential energy in joules*/ static double calculateGravitationalPotentialEnergy(double altitude);

static double calculateGravitationalPotentialEnergy(double altitude);

  • [ ] 前置條件

  • [ ] 后置條件

  • [x] 是前置條件也是后置條件

  • [ ] 都不是

@return potential energy in Joules

  • [ ] 前置條件

  • [x] 后置條件

  • [ ] 是前置條件也是后置條件

  • [ ] 都不是

@param altitude altitude in meters relative to sea level

  • [x] 前置條件

  • [ ] 后置條件

  • [ ] 是前置條件也是后置條件

  • [ ] 都不是

Calculate the potential energy of a mass in Earth's gravitational field.

  • [ ] 前置條件
  • [ ] 后置條件
  • [ ] 是前置條件也是后置條件
  • [x] 都不是

Null 引用

在Java中,對(duì)于對(duì)象和數(shù)組的引用可以取一個(gè)特殊的值null ,它表示這個(gè)這個(gè)引用還沒(méi)有指向任何對(duì)象。Null值在Java類(lèi)型系統(tǒng)中是一個(gè)“不幸的黑洞”。

原始類(lèi)型不能是null :

int size = null; // illegal double depth = null; // illegal

我們可以給非原始類(lèi)型的變量賦予null值:

String name = null; int[] points = null;

在編譯期的時(shí)候,這是合法的。但是如果你嘗試調(diào)用這個(gè)null對(duì)象的方法或者訪問(wèn)它里面對(duì)應(yīng)的數(shù)值,發(fā)產(chǎn)生一個(gè)運(yùn)行時(shí)錯(cuò)誤:

name.length() // throws NullPointerException points.length // throws NullPointerException

要注意是,null并不等于“空”,例如一個(gè)空的字符串""或者一個(gè)空的數(shù)組。對(duì)于一個(gè)空的字符串或者數(shù)組,你可以調(diào)用它們的方法或者訪問(wèn)其中的數(shù)據(jù),只不過(guò)它們對(duì)應(yīng)的元素長(zhǎng)度是0罷了(調(diào)用 length() )。而對(duì)于一個(gè)指向null的String類(lèi)型變量——它什么都不是:調(diào)用 length() 會(huì)產(chǎn)生一個(gè)NullPointer-Exception.

另外要注意一點(diǎn),非原始類(lèi)型的聚合類(lèi)型例如List可能不指向null但是它的元素可能指向null :

String[] names = new String[] { null }; List<Double> sizes = new ArrayList<>(); sizes.add(null);

如果有人嘗試使用這些為null的元素,報(bào)錯(cuò)依然會(huì)發(fā)生。

使用Null值很容易發(fā)生錯(cuò)誤,同時(shí)它們也是不安全的,所以在設(shè)計(jì)程序的時(shí)候盡可能避開(kāi)它們。在這門(mén)課程中——事實(shí)上在大多數(shù)好的Java編程中——一個(gè)約定俗成規(guī)矩就是參數(shù)和返回值不是null。 所以每一個(gè)方法都隱式的規(guī)定了前置條件中數(shù)組或者其他對(duì)象不能是null,同時(shí)后置條件中的返回對(duì)象也不會(huì)是null值(除非規(guī)格說(shuō)明顯式的說(shuō)明了可能返回null,不過(guò)這通常不是一個(gè)好的設(shè)計(jì))。總之,避免使用null!

在Java中你可以在類(lèi)型中顯式的禁用null , 這樣會(huì)在編譯期和運(yùn)行時(shí)自動(dòng)檢查null值:

static boolean addAll(@NonNull List<T> list1, @NonNull List<T> list2)

Google 也對(duì)null的使用進(jìn)行了一些討論,其中說(shuō)到:

不嚴(yán)謹(jǐn)?shù)氖褂胣ull可以導(dǎo)致各種各樣的bug。通過(guò)統(tǒng)計(jì)Google的代碼庫(kù),我們發(fā)現(xiàn)有95%的聚合類(lèi)型不應(yīng)該有任何null值,如果利用這個(gè)性質(zhì)快速失敗的話(huà)比默默接受這些null值更能幫助開(kāi)發(fā)。

另外,null值是有歧義的。通常很難判斷一個(gè)null的返回值意味著什么——例如, Map.get(key) 可能在key對(duì)應(yīng)的value是null的時(shí)候返回null,也可能是因?yàn)関alue不存在而返回null。null可以意味著失敗,也可以意味著成功,它可以是任何東西。使用非null的值能夠使得你的代碼更加清晰易懂。

譯者注:"這是我犯的一個(gè)巨大錯(cuò)誤" - Sir C. A. R. Hoare, null引用的發(fā)明者

閱讀小練習(xí)

NullPointerException accessing exercise.name()

下面哪些變量可以是null ?

  • [ ] int a;

  • [ ] char b;

  • [ ] double c;

  • [x] int[] d;

  • [x] String e;

  • [x] String[] f;

  • [ ] Double g;

  • [x] List<Integer> h;

  • [x] final MouseTrap i;

  • [x] static final String j;

There are null exercises remaining

public static String none() {return null; // (1) }public static void main(String[] args) {String a = none(); // (2)String b = null; // (3)if (a.length() > 0) { // (4)b = a; // (5)}return b; // (6) }

哪一行有靜態(tài)錯(cuò)誤? -> 6

如果們將上一個(gè)問(wèn)題的行注釋掉,然后運(yùn)行 main…

哪一行會(huì)有運(yùn)行時(shí)錯(cuò)誤? -> 4

規(guī)格說(shuō)明應(yīng)該說(shuō)些什么

一個(gè)規(guī)格說(shuō)明應(yīng)該談到接口的參數(shù)和返回的值,但是它不應(yīng)該談到局部變量或者私有的(private)內(nèi)部方法或數(shù)據(jù)。這些內(nèi)部的實(shí)現(xiàn)應(yīng)該在規(guī)格說(shuō)明中對(duì)讀者隱藏。

在Java中,規(guī)格說(shuō)明的讀者通常不會(huì)接觸到實(shí)現(xiàn)的源碼,應(yīng)為Javadoc工具通過(guò)你的源碼自動(dòng)生成對(duì)應(yīng)的規(guī)格說(shuō)明并渲染成HTML。


測(cè)試與規(guī)格說(shuō)明

在測(cè)試中,我們談到了黑盒測(cè)試意味著僅僅通過(guò)規(guī)格說(shuō)明構(gòu)建測(cè)試,而白盒測(cè)試是通過(guò)代碼實(shí)現(xiàn)來(lái)構(gòu)建測(cè)試(譯者注:閱讀03“測(cè)試”)。但是要特別注意一點(diǎn):即使是白盒測(cè)試也必須遵循規(guī)格說(shuō)明。 你的實(shí)現(xiàn)也許很依賴(lài)前置條件的滿(mǎn)足,否則方法就會(huì)有一個(gè)未定義的行為。而你的測(cè)試是不能依賴(lài)這種未定義的行為的。測(cè)試用例必須尊徐規(guī)格說(shuō)明,就像每一個(gè)客戶(hù)一樣。

例如,假設(shè)你正在測(cè)試find,它的規(guī)格說(shuō)明如下:

static int find(int[] arr, int val) - requires:val occurs in arr - effects:returns index i such that arr[i] = val

這個(gè)規(guī)格說(shuō)明已經(jīng)很明顯的要求了前置條件——val必須在arr中存在,而且它的后置條件很“弱”——沒(méi)有規(guī)定返回哪一個(gè)索引,如果在arr中有多個(gè)val的話(huà)。甚至如果你的實(shí)現(xiàn)就是總是返回最后一個(gè)索引,你的測(cè)試用例也不能依賴(lài)這種行為。

int[] array = new int[] { 7, 7, 7 }; assertEquals(0, find(array, 7)); // bad test case: violates the spec assertEquals(7, array[find(array, 7)]); // correct

類(lèi)似的,即使你實(shí)現(xiàn)的find會(huì)在找不到val的時(shí)候拋出一個(gè)異常,你的測(cè)試用例也不能依賴(lài)這種行為,因?yàn)樗荒茉谶`背前置條件的情況下調(diào)用find() 。

那么白盒測(cè)試意味著什么呢?如果它不能違背規(guī)格說(shuō)明的話(huà)?它意味著你可以通過(guò)代碼的實(shí)現(xiàn)去構(gòu)建不同的測(cè)試用例,以此來(lái)測(cè)試不同的實(shí)現(xiàn),但是依然要檢查這些測(cè)試用例符合規(guī)格說(shuō)明。

測(cè)試單元

回想在閱讀03“測(cè)試” 中的web search例子:

/** @return the contents of the web page downloaded from url */ public static String getWebPage(URL url) { ... }/** @return the words in string s, in the order they appear,* where a word is a contiguous sequence of* non-whitespace and non-punctuation characters */ public static List<String> extractWords(String s) { ... }/** @return an index mapping a word to the set of URLs* containing that word, for all webpages in the input set */ public static Map<String, Set<URL>> makeIndex(Set<URL> urls) { ...calls getWebPage and extractWords... }

一個(gè)好的單元測(cè)試應(yīng)該僅僅關(guān)注于一個(gè)規(guī)格說(shuō)明。我們的測(cè)試不應(yīng)該依賴(lài)于另一個(gè)要測(cè)試的單元。例如上面例子中,當(dāng)我們?cè)趯?duì) extractWords 測(cè)試時(shí),就不應(yīng)該使用getWebPage 的輸出作為輸入,因?yàn)槿绻鹓etWebPage 發(fā)生了錯(cuò)誤, extractWords 的行為很可能是未定義的。

而對(duì)于一個(gè)好的綜合測(cè)試(測(cè)試多個(gè)模塊),它確保的是各個(gè)模塊之間是兼容的:調(diào)用者和被調(diào)用者之間的數(shù)據(jù)輸入輸出應(yīng)該是符合要求的。同時(shí)綜合測(cè)試不能取代系統(tǒng)的單元測(cè)試,因?yàn)楦鱾€(gè)模塊的輸出集合很可能在輸入空間中沒(méi)有代表性。例如我們只通過(guò)調(diào)用 makeIndex測(cè)試extractWords .而extractWords的輸出又不能覆蓋掉 makeIndex的很多輸入空間,這樣我們以后在別處復(fù)用 makeIndex的時(shí)候,就很可能產(chǎn)生意想不到的錯(cuò)誤。


改變對(duì)象方法的規(guī)格說(shuō)明

我們?cè)谥暗拈喿x材料中談到了可改變的對(duì)象 vs. 不可改變的對(duì)象。但是我們對(duì)于find的規(guī)格說(shuō)明(后置條件)并沒(méi)有告訴我們這個(gè)副作用——對(duì)象的內(nèi)容被改變了。

以下是一個(gè)告訴了這種作用的規(guī)格說(shuō)明,它來(lái)自Java中 List接口:

static boolean addAll(List<T> list1, List<T> list2) - requires:list1 != list2 - effects:modifies list1 by adding the elements of list2 to the end of it, and returns true if list1 changed as a result of call

首先看看后置條件,它給出了兩個(gè)限制:list1會(huì)被更改;返回值是怎么確定的。

再來(lái)看看前置條件,我們可以發(fā)現(xiàn),如果我們?cè)囍鴮⒁粋€(gè)列表加到它本身,其結(jié)果是未定義的(即規(guī)格說(shuō)明未指出)。這也很好理解,這樣的限制可以使得實(shí)現(xiàn)更容易,例如我們可以將第二個(gè)列表的元素逐個(gè)加入到第一個(gè)列表中。如果嘗試將兩個(gè)指向同一個(gè)對(duì)象的列表相加,就可能發(fā)生下圖的情況,即將列表2的元素添加到列表1中后同時(shí)也改變了列表2,這樣方法可能不會(huì)終止(或者最終內(nèi)存不夠而拋出異常):

另外,上文“Null 引用”提到過(guò),這還有一個(gè)隱含的前置條件:list1和list2都不是null ,。

這里有另一個(gè)改變對(duì)象方法的例子:

static void sort(List<String> lst) - requires:nothing - effects:puts lst in sorted order, i.e. lst[i] ≤ lst[j] for all 0 ≤ i < j < lst.size()

和一個(gè)不改變對(duì)象方法的例子:

static List<String> toLowerCase(List<String> lst) - requires:nothing - effects:returns a new list t where t[i] = lst[i].toLowerCase()

正如null是隱式的不被允許的,我們也隱式的規(guī)定改變對(duì)象(mutation)是不被允許的,除非顯式的聲明 。例如 to-Lower-Case 的規(guī)格說(shuō)明中就沒(méi)有談到該方法會(huì)不會(huì)改變參數(shù)對(duì)象(不會(huì)改變),而sort中就顯式的說(shuō)明了。

READING EXERCISES閱讀小練習(xí)

What’s in a spec?

下面哪一些選項(xiàng)是屬于規(guī)格說(shuō)明的?

  • [x] 返回類(lèi)型

  • [x] 返回值的范圍

  • [x] 參數(shù)個(gè)數(shù)

  • [x] 參數(shù)種類(lèi)

  • [x] 對(duì)參數(shù)的限制

gcd 1

Alice 寫(xiě)了如下代碼:

public static int gcd(int a, int b) {if (a > b) {return gcd(a-b, b);} else if (b > a) {return gcd(a, b-a);}return a; }

Bob 寫(xiě)了如下對(duì)應(yīng)測(cè)試:

@Test public void gcdTest() {assertEquals(6, gcd(24, 54)); }

測(cè)試通過(guò)了!以下哪些說(shuō)法是正確的?

Alice 應(yīng)該在前置條件中加上 a > 0 -> True

Alice 應(yīng)該在前置條件中加上 b > 0 -> True

Alice 應(yīng)該在后置條件中加上 gcd(a, b) > 0 -> False

Alice 應(yīng)該在后置條件中加上 a and b are integers -> False

gcd 2

如果Alice 在前置條件中加上 a > 0 , Bob 應(yīng)該測(cè)試負(fù)數(shù) a -> False

如果Alice 沒(méi)有在前置條件中加上 a > 0 , Bob 應(yīng)該測(cè)試負(fù)數(shù) a -> True


異常

現(xiàn)在我們來(lái)討論一下如何處理異常的情況,并且這種處理既能遠(yuǎn)離bug又能易于理解。

一個(gè)方法的標(biāo)識(shí)(signature)包含它的名字、參數(shù)類(lèi)型、返回類(lèi)型,同時(shí)也包含該方法能觸發(fā)的異常。

參考閱讀: Exceptions in the Java Tutorials.

報(bào)告bug的異常

你可能已經(jīng)在Java編程中遇到了一些異常,例如 ArrayIndex-OutOfBounds-Exception (數(shù)組訪問(wèn)越界)或者 Null-Pointer-Exception (訪問(wèn)一個(gè)null引用的對(duì)象)。這些異常通常都是用來(lái)報(bào)告你代碼里的bug ,同時(shí)它們報(bào)告的信息也能幫助你修復(fù)bug。

ArrayIndex-OutOfBounds- 和 Null-Pointer-Exception 大概是最常見(jiàn)的異常了,其他的例子有:

  • ArithmeticException, 當(dāng)發(fā)生計(jì)算錯(cuò)誤時(shí)拋出,例如除0。
  • NumberFormatException, 數(shù)字的類(lèi)型不匹配的時(shí)候拋出,例如你向Integer.parseInt 傳入一個(gè)字符長(zhǎng)而不是一個(gè)整數(shù)。

報(bào)告特殊結(jié)果的異常

異常不僅被用來(lái)報(bào)告bug,它們也被用來(lái)提升那些包含特殊結(jié)果的代碼的結(jié)構(gòu)。

不幸的是,一個(gè)常見(jiàn)的處理特殊結(jié)果的方法就是返回一個(gè)特殊的值。你在Java庫(kù)中常常能發(fā)現(xiàn)這樣的設(shè)計(jì):當(dāng)你期望一個(gè)正整數(shù)的時(shí)候,特殊結(jié)果會(huì)返回一個(gè)-1;當(dāng)你期望一個(gè)對(duì)象的時(shí)候,特殊結(jié)果會(huì)返回一個(gè)null 。這樣的方法如果謹(jǐn)慎使用也還OK,但是它有兩個(gè)問(wèn)題。首先,它加重的檢查返回值的負(fù)擔(dān)。其次,程序員很可能會(huì)忘記檢查返回值(我們待會(huì)會(huì)看到通過(guò)使用異常,編譯器會(huì)幫助你處理這些問(wèn)題)。

同時(shí),找到一個(gè)“特殊值”返回并不是一件容易的事?,F(xiàn)在假設(shè)我們有一個(gè) BirthdayBook 類(lèi),其中有一個(gè)lookup方法:

class BirthdayBook {LocalDate lookup(String name) { ... } }

(LocalDate 是Java API的一個(gè)類(lèi).)

如果name在這個(gè)BirthdayBook中沒(méi)有入口,這個(gè)方法該如何返回呢?或許我們可以找一個(gè)永遠(yuǎn)不會(huì)被人用到的日期。糟糕的程序員或許會(huì)選擇一個(gè)9/9/99,畢竟他們覺(jué)得沒(méi)有人會(huì)在這個(gè)世紀(jì)結(jié)束的時(shí)候使用這個(gè)程序。((事實(shí)上,它們錯(cuò)了)

這里有一個(gè)更好的辦法,就是拋出一個(gè)異常:

LocalDate lookup(String name) throws NotFoundException {...if ( ...not found... )throw new NotFoundException();...

調(diào)用者使用catch捕獲這個(gè)異常:

BirthdayBook birthdays = ... try {LocalDate birthdate = birthdays.lookup("Alyssa");// we know Alyssa's birthday } catch (NotFoundException nfe) {// her birthday was not in the birthday book }

現(xiàn)在我們就不需要使用“特殊”的返回值來(lái)通報(bào)特殊情況了,調(diào)用者也不需要再檢查返回值。

閱讀小練習(xí)

1st birthday

假設(shè)我們?cè)谑褂?BirthdayBook 中的 lookup 方法,它可能會(huì)拋出 NotFoundException.

如果“Elliot”不在birthdays里面(birthdays已經(jīng)初始化了,并指向了一個(gè)對(duì)象),下面這些代碼會(huì)發(fā)生什么?

try {LocalDate birthdate = birthdays.lookup("Elliot"); }

運(yùn)行時(shí)報(bào)錯(cuò): NotFoundException

2nd birthday

try {LocalDate birthdate = birthdays.lookup("Elliot"); } catch (NotFoundException nfe) {birthdate = LocalDate.now(); }

靜態(tài)錯(cuò)誤: undeclared variable

3rd birthday

try {LocalDate birthdate = birthdays.lookup("Elliot"); } catch (NotFoundException nfe) {throw new DateTimeException("Missing reference birthday", nfe); }

(DateTimeException is provided by the Java API.)

運(yùn)行時(shí)報(bào)錯(cuò): DateTimeException


已檢查(Checked)異常和未檢查(Unchecked)異常

我們已經(jīng)看到了兩種不同目的的異常:報(bào)告特殊的結(jié)果或者報(bào)告bug。一個(gè)通用的規(guī)則是,我們用已檢查的異常來(lái)報(bào)告特殊結(jié)果,用未檢查的異常來(lái)報(bào)告bug。在后面一節(jié)中,我們會(huì)詳細(xì)介紹一些。

“ 已檢查 異常”這個(gè)名字是因?yàn)榫幾g器會(huì)檢查這種異常是否被正確處理:

  • 如果一個(gè)方法拋出一個(gè)已檢查異常,這種可能性必須添加到它的標(biāo)識(shí)中。例如 Not-Found-Exception就是一個(gè)已檢查異常,這也是為什么它的生命的結(jié)尾有一個(gè) throws Not-Found-Exception.
  • 如果一個(gè)方法調(diào)用一個(gè)可能拋出已檢查異常的方法,該方法要么處理它,要么在它的標(biāo)識(shí)中說(shuō)明該異常(交給它的調(diào)用者處理)。

所以如果你調(diào)用了 BirthdayBook中的 lookup 并忘記處理 Not-Found-Exception ,編譯器就會(huì)拒絕你的代碼。這非常有用,因?yàn)樗_保了那些可能產(chǎn)生的特殊情況(異常)被處理。

相應(yīng)的,未檢查異常用來(lái)報(bào)告bug。這些異常并不指望被代碼處理(除了一些頂層的代碼),同時(shí)這樣的異常也不應(yīng)該被顯式拋出,例如邊界溢出、null值、非法參數(shù)、斷言失敗等等。同樣,編譯器不會(huì)檢查這些異常是否被 try-catch 處理或者用 throws 拋給上一層調(diào)用者。(Java允許你將未檢查的異常作為方法的標(biāo)識(shí),不過(guò)這沒(méi)有什么意義,我們也不建議這么做)

異常中有可能有和異常相關(guān)的信息。(如果構(gòu)建體沒(méi)有提供,引用這個(gè)信息(String)的值將會(huì)是null )

Throwable 類(lèi)層次

為了理解Java是如何定義一個(gè)異常是已檢查還是未檢查的,讓我們看一看Java異常類(lèi)的層次圖:

Throwable 是一個(gè)能夠被拋出和捕獲的對(duì)象對(duì)應(yīng)的類(lèi)。Throwable的實(shí)現(xiàn)記錄了棧的結(jié)構(gòu)(異常被拋出的時(shí)候),同時(shí)還有一個(gè)描述該異常的消息(可選)。任何被拋出或者捕獲的異常對(duì)象都應(yīng)該是 Throwable的子類(lèi)。

Error 是 Throwable 的一個(gè)子類(lèi),它被保留用于Java運(yùn)行系統(tǒng)的異常,例如 StackOverflow-Error 和 OutOfMemory-Error.Errors應(yīng)該被認(rèn)為是不可恢復(fù)的,并且一般不會(huì)去捕獲它。(這里有一個(gè)特例, Assertion-Error 也是屬于Error 的,即使它反映的是用戶(hù)代碼錯(cuò)誤)

下面描述了在Java中如何區(qū)別已檢查異常和未檢查異常:

  • RuntimeException, Error, 以及它們的子類(lèi)都是未檢查異常。編譯器不會(huì)要求它們被throws修飾,也不會(huì)要求它們被捕獲。
  • 所有其他的throwables—— Throwable, Exception和其他子類(lèi)都是已檢查異常。編譯器會(huì)要求它們被捕獲或者用throws傳給調(diào)用者處理。

當(dāng)你定義你自己的異常時(shí),你應(yīng)該使它要么是 RuntimeException 的子類(lèi)(未檢查異常),要么是 Exception 的子類(lèi)(已檢查異常)。程序員通常不會(huì)生成 Error 或者 Throwable的子類(lèi),因?yàn)樗鼈兺ǔ1籎ava保留使用。

閱讀小練習(xí)

Get to the point

假設(shè)我們寫(xiě)了一個(gè)尋找兩點(diǎn)之間路徑的方法:

public static List<Point> findPath(Point initial, Point goal)

In the postcondition, we say that findPath will search for paths only up to a bounded length (set elsewhere), and that it will throw an exception if it fails to find one.在前置條件中,我們要求findPath 搜索的范圍是有限的(有邊界)。如果該方法沒(méi)有找到一個(gè)路徑,它就會(huì)拋出一個(gè)異常。

在設(shè)計(jì)方法時(shí),以下哪一個(gè)異常是合理的?

  • [ ] 已檢查異常 NoPathException
  • [ ] 未檢查異常 NoPathException
  • [x] 已檢查異常 PathNotFoundException
  • [ ] 未檢查異常 PathNotFoundException

Don’t point that thing at me

當(dāng)我們定義該異常時(shí),應(yīng)該使它是哪一個(gè)類(lèi)的子類(lèi)?

  • [ ] Throwable
  • [x] Exception
  • [ ] Error
  • [ ] RuntimeException


設(shè)計(jì)異常時(shí)應(yīng)該考慮的事情

我們之前給了一個(gè)通用規(guī)則——對(duì)于特殊的結(jié)果(預(yù)測(cè)到的)使用已檢查異常,對(duì)于bug使用未檢查異常(意料之外)。這說(shuō)得通,不過(guò),在Java中異常并沒(méi)有這么“輕量化”。

除了對(duì)性能有影響,Java中的異常會(huì)帶來(lái)使用上的開(kāi)銷(xiāo):如果你要設(shè)計(jì)一個(gè)異常,你必須創(chuàng)建一個(gè)新的類(lèi)。如果你調(diào)用一個(gè)可能拋出已檢查異常的方法,你必須使用 try-catch 處理它(即使你知道這個(gè)異常一定不會(huì)發(fā)生)。后一種情況導(dǎo)致了一個(gè)進(jìn)退兩難的局面。例如,你設(shè)計(jì)了一個(gè)抽象隊(duì)列,你是應(yīng)該期望使用者在循環(huán)pop的時(shí)候檢查隊(duì)列是否為空(作為前置條件),還是讓使用者自由的pop,最后拋出一個(gè)異常呢?如果你選擇拋出異常,那么即使使用者每次都檢查隊(duì)列不為空才pop,他還是要對(duì)這個(gè)異常進(jìn)行處理。

所以我們提煉出另一個(gè)明確的規(guī)則:

  • 對(duì)于意料之外的bug使用未檢查的異常,或者對(duì)于使用者來(lái)說(shuō)避免異常產(chǎn)生的情況非常容易(例如檢查一個(gè)隊(duì)列是否為空)。
  • 其他的情況我們使用已檢查異常。

這里舉出一些例子:

  • 當(dāng)隊(duì)列是空時(shí),Queue.pop() 會(huì)拋出一個(gè)未檢查異常。因?yàn)闄z查隊(duì)列是否為空對(duì)于用戶(hù)來(lái)說(shuō)是容易的。(例如 Queue.size() or Queue.isEmpty().)
  • 當(dāng)無(wú)法連接互聯(lián)網(wǎng)時(shí),Url.getWebPage() 拋出一個(gè)已檢查異常 IOException ,因?yàn)榭蛻?hù)可能無(wú)法確定調(diào)用的時(shí)候網(wǎng)絡(luò)是否好使。
  • 當(dāng)x沒(méi)有整數(shù)開(kāi)方時(shí),int integerSquareRoot(int x) 拋出一個(gè)已檢查異常 Not-Perfect-Square-Exception ,因?yàn)閷?duì)于調(diào)用者來(lái)說(shuō),判斷一個(gè)整數(shù)是否為平方是困難的。

這些使用異常的“痛楚”也是很多Java API使用null引用或特殊值作為返回值的原因。額.....如果你嚴(yán)謹(jǐn)認(rèn)真的使用這些返回值,這也不是什么糟糕的事情。

在規(guī)格說(shuō)明中應(yīng)該如何聲明異常

因?yàn)楫惓R部梢詺w為方法的輸出,所以我們應(yīng)該在規(guī)格說(shuō)明的后置條件中描述它。Java中是以 @throws 作為Javadoc中異常注釋的。Java也可能要求函數(shù)聲明時(shí)用throws標(biāo)出可能拋出的異常 。這一節(jié)會(huì)討論什么時(shí)候使用這兩種方法。

對(duì)于非檢查的異常,由于它們描述的是意料之外的bug或者失敗,不屬于后置條件,所以不應(yīng)該用 @throws 或 throws修飾它們。例如, NullPointerException就不應(yīng)該在規(guī)格說(shuō)明中列出——我們的前置條件已經(jīng)隱式(顯式)的禁止了null值,這意味著如果使用者傳入一個(gè)null,我們可以沒(méi)有任何警告的扔出一個(gè)異常。例如下面這個(gè)規(guī)格說(shuō)明,就沒(méi)有提到 NullPointerException :

/*** @param lst list of strings to convert to lower case* @return new list lst' where lst'[i] is lst[i] converted to lowercase*/ static List<String> toLowerCase(List<String> lst)

而對(duì)于報(bào)告特殊結(jié)果的異常,我們應(yīng)該在Javadoc中用 @throws 表示出來(lái),并明確什么情況下會(huì)導(dǎo)致這個(gè)異常的拋出。另外,如果是一個(gè)已檢查異常,Java會(huì)要求在函數(shù)聲明的時(shí)候用 throws 標(biāo)識(shí)出來(lái)。例如,假設(shè) NotPerfectSquareException 是一個(gè)已檢查聲明:

/*** Compute the integer square root.* @param x value to take square root of* @return square root of x* @throws NotPerfectSquareException if x is not a perfect square*/ int integerSquareRoot(int x) throws NotPerfectSquareException;

對(duì)于報(bào)告特殊結(jié)果的未檢查異常,Java允許但是不要求使用 throws 在聲明中標(biāo)識(shí)出。但是這種情況下通常不要使用 throws 因?yàn)檫@會(huì)使得閱讀者困惑(以為它是一個(gè)已檢查異常)。例如,假設(shè)你將EmptyQueueException定義為未檢查異常。那么你應(yīng)該在Javadoc中使用 @throws對(duì)其進(jìn)行說(shuō)明,但是不要在函數(shù)聲明中將其標(biāo)識(shí)出:

/*** Pops a value from this queue.* @return next value in the queue, and removes the value from the queue* @throws EmptyQueueException if this queue is empty*/ int pop();

閱讀小練習(xí)

Throw all the things!

閱讀以下代碼并分析 Thing 對(duì)象:

static Set<Thing> ALL_THE_THINGS;static void analyzeEverything() {analyzeThingsInOrder(); }static void analyzeThingsInOrder() {try {for (Thing t : ALL_THE_THINGS) {analyzeOneThing(t);}} catch (AnalysisException ae) {return;} }static void analyzeOneThing(Thing t) throws AnalysisException {// ...// ... maybe go off the end of an array// ... }

AnalysisException 是一個(gè) 已檢查 異常.

analyzeEverything可能會(huì)拋出哪一些異常?

  • [x] ArrayIndexOutOfBoundsException

  • [ ] IOException

  • [x] NullPointerException

  • [ ] AnalysisException

  • [ ] OutOfMemoryError

A terrible thing

如果 analyzeOneThing 自己會(huì)拋出一個(gè) AnalysisException 異常,會(huì)發(fā)生什么?

  • [ ] 程序可能會(huì)崩潰

  • [x] 我們可能不能調(diào)用任何 analyzeOneThing

  • [ ] 我們可能會(huì)調(diào)用幾次 analyzeOneThing


總結(jié)

最后,再做一組練習(xí)看看你對(duì)今天學(xué)的內(nèi)容理解的如何。

閱讀小練習(xí)

拼字游戲 1

/* Requires: tiles has length 7 & contains only uppercase letters.crossings contains only uppercase letters, without duplicates.Effects: Returns a list of words where each word can be made by takingletters from tiles and at most 1 letter from crossings.*/ public static List<String> scrabble(String tiles, String crossings) {if (tiles.length() != 7) { throw new RuntimeException(); }return new ArrayList<>(); }

scrabble的后置條件有哪些?

  • [ ] tiles 中只有大寫(xiě)字母
  • [ ] crossings 中字母沒(méi)有重復(fù)
  • [ ] scrabble 需要兩個(gè)參數(shù)
  • [x] scrabble 返回字符串列表

scrabble的前置條件有哪些?

  • [x] tiles 長(zhǎng)度為 7
  • [x] crossings 是一個(gè)大寫(xiě)的字符串
  • [x] scrabble參數(shù)的類(lèi)型是 String 和 String
  • [ ] scrabble 返回一個(gè)空的 ArrayList

拼字游戲 2

規(guī)格說(shuō)明中的哪一部分是會(huì)被靜態(tài)檢查的?

  • [ ] tiles 中只有大寫(xiě)字母
  • [ ] crossings 中字母沒(méi)有重復(fù)
  • [ ] 當(dāng) tiles.length() != 7, scrabble 拋出 RuntimeException
  • [x] scrabble 接收兩個(gè)參數(shù)

scrabble 的實(shí)現(xiàn)滿(mǎn)足了規(guī)格說(shuō)明嗎?

  • [ ] 是
  • [ ] 否, 因?yàn)樗鼤?huì)在無(wú)法獲取tiles長(zhǎng)度時(shí)拋出 RuntimeException
  • [x] 否,因?yàn)榧词刮覀儌魅胍粋€(gè)可以組合成詞的tiles和crossings,它也會(huì)返回一個(gè)空列表。

一個(gè)規(guī)格說(shuō)明就好像是實(shí)現(xiàn)者和使用者之間的防火墻。它使得分別開(kāi)發(fā)成為可能:使用者可以在不理解源代碼的情況下使用模塊,實(shí)現(xiàn)者可以在不知道模塊如何被使用的情況下實(shí)現(xiàn)模塊。

現(xiàn)在讓我們想想今天的內(nèi)容和我們?nèi)竽繕?biāo)之間的聯(lián)系:

  • 遠(yuǎn)離bug. 一個(gè)好的規(guī)格說(shuō)明會(huì)清晰明確的要求實(shí)現(xiàn)者和使用者遵守相關(guān)的制約。而B(niǎo)ug經(jīng)常是因?yàn)閷?shí)現(xiàn)者和使用者對(duì)于接口的理解沖突導(dǎo)致的,規(guī)格說(shuō)明會(huì)明顯的減小這種可能性。在模塊中使用一些能夠交由機(jī)器檢查的特性,例如靜態(tài)檢查、異常等而不是注釋會(huì)進(jìn)一步降低bug的可能性。
  • 易讀性. 一個(gè)簡(jiǎn)潔準(zhǔn)確的規(guī)格說(shuō)明會(huì)比源代碼本身更易讀易懂。
  • 可改動(dòng)性. 規(guī)格說(shuō)明在實(shí)現(xiàn)者和使用者之間建立了一個(gè)“契約”——只要這兩方遵守這份“契約”,他們可以對(duì)自己的代碼進(jìn)行任何改變。

轉(zhuǎn)載于:https://www.cnblogs.com/liqiuhao/p/8566500.html

總結(jié)

以上是生活随笔為你收集整理的麻省理工18年春软件构造课程阅读06“规格说明”的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。