麻省理工18年春软件构造课程阅读06“规格说明”
本文內容來自MIT_6.031_sp18: Software Construction課程的Readings部分,采用CC BY-SA 4.0協議。
由于我們學校(哈工大)大二軟件構造課程的大部分素材取自此,也是推薦的閱讀材料之一,于是打算做一些翻譯工作,自己學習的同時也能幫到一些懶得看英文的朋友。另外,該課程的閱讀資料中有許多練習題,但是沒有標準答案,所給出的答案均為譯者所寫,有錯誤的地方還請指出。
譯者:李秋豪
審校:
V1.0 Tue Mar 13 22:17:35 CST 2018
本次課程的目標
- 理解方法規格說明中的前置條件和后置條件,并能夠寫出正確的規格說明
- B能夠針對規格說明寫出測試
- 理解Java中的檢查異常和非檢查異常(checked and unchecked exceptions)
- 理解如何用異常處理特殊的結果
概要
規格說明是團隊合作中的關鍵點。如果沒有規格說明,就沒有辦法分工實現各種方法。規格說明就像一份合同:實現者的義務在于滿足合同的要求,客戶可以依賴這些要求工作。事實上,我們會發現就像真的合同一樣,規格說明對雙方都有制約:當合同上有前置條件時,客戶有責任滿足這些條件。
在這篇閱讀材料中我們會研究方法中的規格說明,討論前置條件和后置條件分別是什么,它們對方法的實現者和使用者來說意味著什么。我們也會討論如何使用異?!狫ava、Python、以及很多現代語言中的一個重要特性,它使得方法的接口更加安全也更加易懂。
為什么要使用規格說明
在編程中,很多讓人抓狂的bug是由于兩個地方的代碼對于接口行為的理解不一樣。雖然每一個程序員在心里都有一份“規格說明”,但是不是所有程序員都會把他們寫下來。最終,一個團隊中的不同程序員對于同一個接口就有不同的“規格說明”了。當程序崩潰的時候,就很難發現問題在哪里。簡潔準確的的規格說明使得我們遠離bug,更可以快速發現問題所在。
規格說明對使用者(客戶)來說也是很有用的,它們使得使用者不必去閱讀源碼。如果你還不相信閱讀規格說明比閱讀源碼更簡單易懂的話,看看下面這個標準的Java規格說明和它對應的源碼,它是 BigInteger 中的一個方法:
API 文檔中的規格說明:
public BigInteger add(BigInteger val)Returns a BigInteger whose value is (this + val).Parameters: val - value to be added to this BigInteger.Returns: this + valJava 8 中對應的源碼:
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);可以看到,通過閱讀 BigInteger.add 的規格說明,客戶可以直接了解如何使用 BigInteger.add ,以及它的行為屬性。如果我們去閱讀源碼,我們就不得不看 BigInteger 的構造體, compare-Magnitude, subtract以及trusted-StripLeadingZero-Ints 的實現——而這還僅僅只是開始。
另外,規格說明對于實現者也是很有好處的,因為它們給了實現者更改實現策略而不告訴使用者的自由。同時,規格說明可以限定一些特殊的輸入,這樣實現者就可以省略一些麻煩的檢查和處理,代碼也可以運行的更快。
如上圖所示,規格說明就好像一道防火墻一樣將客戶和實現者隔離開。它使得客戶不必知道這個單元是如何運行的(不必閱讀源碼),也使得實現者不必管這個單元會被怎么使用(因為客戶要遵守前置條件)。這種隔離造成了“解耦”(decoupling),客戶自己的代碼和實現者的代碼可以獨立發生改動,只要雙方都遵循規格說明對應的制約。
行為等價
思考下面兩個方法的異同:
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; }當然,這兩個方法的代碼是不同的,名字的含義也不一樣。為了判斷“行為等價”,我們必須判斷一個方法是否可以替換另一個方法,而程序的行為不發生改變。
除了代碼,它們的行為也不一樣:
- 當val找不到時,fingFirst返回arr的長度而findLast返回-1;
- 當數組中有兩個val的時候,findFirst返回較小的那個索引,而findLast返回較大的那個。
但是當val在數組中僅有一個的時候,這兩個方法的行為是一樣的。也只有在這種情況下,我們才可以將方法的實現在兩者中互換。
“行為等價”是對于“旁觀者”來說的——就是客戶。為了讓實現方法可以發生改動,我們就需要一個規格說明要求客戶遵守某一些制約/前置條件。
所以,我們的規格說明可能是這樣的:
static int find(int[] arr, int val) - requires:val occurs exactly once in arr - effects:returns index i such that arr[i] = val閱讀小練習
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; }假設客戶只關心val是否在a中出現了一次。在這種情況下,findFirst 和 findLast 的行為等價嗎?
Yes
Best behavior
現在來改變一下規格說明,假設客戶對返回值要求:
- 如果val在a中,返回任何索引i ,使得a[i] == val 。
- 否則,返回一個不在a索引范圍內的整數j
在這種情況下,findFirst 和 findLast 的行為等價嗎?
Yes
規格說明的結構
一個規格說明含有以下兩個“條款”:
- 一個前置條件,關鍵詞是requires
- 一個后置條件,關鍵詞是effects
其中前置條件是客戶的義務(誰調用的這個方法)。它確保了方法被調用時所處的狀態。
而后置條件是實現者的義務。如果前置條件得到了滿足,那么該方法的行為應該符合后置條件的要求,例如返回一個合適的值,拋出一個特定的異常,修改一個特定的對象等等。
如果前置條件不滿足的話,實現也不需要滿足后置條件——方法可以做任何事情,例如不終止而是拋出一個異常、返回一個任意的值、做一個任意的修改等等。
閱讀小練習
Logical implication
思考下面這個規格說明
static int find(int[] arr, int val) - requires:val occurs exactly once in arr - effects:returns index i such that arr[i] = val作為find的實現者,下面哪些行為是合法的?
[x] 如果arr為空,返回0
[x] 如果arr為空,拋出一個異常
[x] 如果val在arr出現了兩次,拋出一個異常
[x] 如果val在arr出現了兩次,將arr中的元素都設置為0,然后拋出一個異常
[x] 如果arr不為空但是val沒有出現,選取一個隨機的索引,將其對應的元素設置為val ,然后返回這個索引
[x] 如果arr[0]是val ,繼續檢查剩下的元素,返回索引最高的那個val對飲的索引(沒有再次找到val就返回0)
Logical implementation
作為find的實現者,當arr為空的時候,為什么要拋出一個異常?
- [ ] DRY(譯者注:Don't repeat yourself)
- [x] 快速失敗/報錯
- [ ] 避免幻數
- [ ] 一個變量只有一個目的
- [ ] 避免全局變量
- [ ] 返回結果
Java中的規格說明
有一些語言(例如 Eiffel ),將前置條件和后置條件作為語言的基礎之一,以便程序運行的時候(或者編譯器)可以自動檢查客戶和實現者是否都遵循了規格說明。
Java并沒有這么嚴格,但是它的靜態檢查也是屬于一種前置條件和后置條件的檢查(編譯器)。至于剩下的部分——那些不屬于數據類型范疇的約束——必須通過注釋寫在方法的前面,通過人們來檢查和保證。
Java對于 文檔注釋有一些傳統,例如參數的說明以 @param作為開頭,返回的說明以@return 作為開頭。你應該將前置條件放在@param 的地方,后置條件放在 @return的地方。例如,一個規格說明可能是這樣:
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 文檔 就是通過Java標準庫源碼中的規格說明注釋生成的. 同樣的,Eclipse也可以根據你的規格說明產生對應的文檔),或者產生和Java API一個格式的 HTML 文檔 ,這對你和你的客戶來說都是很有用的信息。
參考閱讀:
Java: Javadoc Comments
Oracle: How to Write Doc Comments
閱讀小練習
Javadoc
思考以下規格說明:
static boolean isPalindrome(String word) - requires:word contains only alphanumeric characters - effects:returns true if and only if word is a palindrome對應的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*/請問Javadoc中哪一行是有問題的?
- [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
思考下面這個規格說明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中,對于對象和數組的引用可以取一個特殊的值null ,它表示這個這個引用還沒有指向任何對象。Null值在Java類型系統中是一個“不幸的黑洞”。
原始類型不能是null :
int size = null; // illegal double depth = null; // illegal我們可以給非原始類型的變量賦予null值:
String name = null; int[] points = null;在編譯期的時候,這是合法的。但是如果你嘗試調用這個null對象的方法或者訪問它里面對應的數值,發產生一個運行時錯誤:
name.length() // throws NullPointerException points.length // throws NullPointerException要注意是,null并不等于“空”,例如一個空的字符串""或者一個空的數組。對于一個空的字符串或者數組,你可以調用它們的方法或者訪問其中的數據,只不過它們對應的元素長度是0罷了(調用 length() )。而對于一個指向null的String類型變量——它什么都不是:調用 length() 會產生一個NullPointer-Exception.
另外要注意一點,非原始類型的聚合類型例如List可能不指向null但是它的元素可能指向null :
String[] names = new String[] { null }; List<Double> sizes = new ArrayList<>(); sizes.add(null);如果有人嘗試使用這些為null的元素,報錯依然會發生。
使用Null值很容易發生錯誤,同時它們也是不安全的,所以在設計程序的時候盡可能避開它們。在這門課程中——事實上在大多數好的Java編程中——一個約定俗成規矩就是參數和返回值不是null。 所以每一個方法都隱式的規定了前置條件中數組或者其他對象不能是null,同時后置條件中的返回對象也不會是null值(除非規格說明顯式的說明了可能返回null,不過這通常不是一個好的設計)。總之,避免使用null!
在Java中你可以在類型中顯式的禁用null , 這樣會在編譯期和運行時自動檢查null值:
static boolean addAll(@NonNull List<T> list1, @NonNull List<T> list2)Google 也對null的使用進行了一些討論,其中說到:
不嚴謹的使用null可以導致各種各樣的bug。通過統計Google的代碼庫,我們發現有95%的聚合類型不應該有任何null值,如果利用這個性質快速失敗的話比默默接受這些null值更能幫助開發。
另外,null值是有歧義的。通常很難判斷一個null的返回值意味著什么——例如, Map.get(key) 可能在key對應的value是null的時候返回null,也可能是因為value不存在而返回null。null可以意味著失敗,也可以意味著成功,它可以是任何東西。使用非null的值能夠使得你的代碼更加清晰易懂。
譯者注:"這是我犯的一個巨大錯誤" - Sir C. A. R. Hoare, null引用的發明者
閱讀小練習
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) }哪一行有靜態錯誤? -> 6
如果們將上一個問題的行注釋掉,然后運行 main…
哪一行會有運行時錯誤? -> 4
規格說明應該說些什么
一個規格說明應該談到接口的參數和返回的值,但是它不應該談到局部變量或者私有的(private)內部方法或數據。這些內部的實現應該在規格說明中對讀者隱藏。
在Java中,規格說明的讀者通常不會接觸到實現的源碼,應為Javadoc工具通過你的源碼自動生成對應的規格說明并渲染成HTML。
測試與規格說明
在測試中,我們談到了黑盒測試意味著僅僅通過規格說明構建測試,而白盒測試是通過代碼實現來構建測試(譯者注:閱讀03“測試”)。但是要特別注意一點:即使是白盒測試也必須遵循規格說明。 你的實現也許很依賴前置條件的滿足,否則方法就會有一個未定義的行為。而你的測試是不能依賴這種未定義的行為的。測試用例必須尊徐規格說明,就像每一個客戶一樣。
例如,假設你正在測試find,它的規格說明如下:
static int find(int[] arr, int val) - requires:val occurs in arr - effects:returns index i such that arr[i] = val這個規格說明已經很明顯的要求了前置條件——val必須在arr中存在,而且它的后置條件很“弱”——沒有規定返回哪一個索引,如果在arr中有多個val的話。甚至如果你的實現就是總是返回最后一個索引,你的測試用例也不能依賴這種行為。
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類似的,即使你實現的find會在找不到val的時候拋出一個異常,你的測試用例也不能依賴這種行為,因為它不能在違背前置條件的情況下調用find() 。
那么白盒測試意味著什么呢?如果它不能違背規格說明的話?它意味著你可以通過代碼的實現去構建不同的測試用例,以此來測試不同的實現,但是依然要檢查這些測試用例符合規格說明。
測試單元
回想在閱讀03“測試” 中的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... }一個好的單元測試應該僅僅關注于一個規格說明。我們的測試不應該依賴于另一個要測試的單元。例如上面例子中,當我們在對 extractWords 測試時,就不應該使用getWebPage 的輸出作為輸入,因為如果getWebPage 發生了錯誤, extractWords 的行為很可能是未定義的。
而對于一個好的綜合測試(測試多個模塊),它確保的是各個模塊之間是兼容的:調用者和被調用者之間的數據輸入輸出應該是符合要求的。同時綜合測試不能取代系統的單元測試,因為各個模塊的輸出集合很可能在輸入空間中沒有代表性。例如我們只通過調用 makeIndex測試extractWords .而extractWords的輸出又不能覆蓋掉 makeIndex的很多輸入空間,這樣我們以后在別處復用 makeIndex的時候,就很可能產生意想不到的錯誤。
改變對象方法的規格說明
我們在之前的閱讀材料中談到了可改變的對象 vs. 不可改變的對象。但是我們對于find的規格說明(后置條件)并沒有告訴我們這個副作用——對象的內容被改變了。
以下是一個告訴了這種作用的規格說明,它來自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首先看看后置條件,它給出了兩個限制:list1會被更改;返回值是怎么確定的。
再來看看前置條件,我們可以發現,如果我們試著將一個列表加到它本身,其結果是未定義的(即規格說明未指出)。這也很好理解,這樣的限制可以使得實現更容易,例如我們可以將第二個列表的元素逐個加入到第一個列表中。如果嘗試將兩個指向同一個對象的列表相加,就可能發生下圖的情況,即將列表2的元素添加到列表1中后同時也改變了列表2,這樣方法可能不會終止(或者最終內存不夠而拋出異常):
另外,上文“Null 引用”提到過,這還有一個隱含的前置條件:list1和list2都不是null ,。
這里有另一個改變對象方法的例子:
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()和一個不改變對象方法的例子:
static List<String> toLowerCase(List<String> lst) - requires:nothing - effects:returns a new list t where t[i] = lst[i].toLowerCase()正如null是隱式的不被允許的,我們也隱式的規定改變對象(mutation)是不被允許的,除非顯式的聲明 。例如 to-Lower-Case 的規格說明中就沒有談到該方法會不會改變參數對象(不會改變),而sort中就顯式的說明了。
READING EXERCISES閱讀小練習
What’s in a spec?
下面哪一些選項是屬于規格說明的?
[x] 返回類型
[x] 返回值的范圍
[x] 參數個數
[x] 參數種類
[x] 對參數的限制
gcd 1
Alice 寫了如下代碼:
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 寫了如下對應測試:
@Test public void gcdTest() {assertEquals(6, gcd(24, 54)); }測試通過了!以下哪些說法是正確的?
Alice 應該在前置條件中加上 a > 0 -> True
Alice 應該在前置條件中加上 b > 0 -> True
Alice 應該在后置條件中加上 gcd(a, b) > 0 -> False
Alice 應該在后置條件中加上 a and b are integers -> False
gcd 2
如果Alice 在前置條件中加上 a > 0 , Bob 應該測試負數 a -> False
如果Alice 沒有在前置條件中加上 a > 0 , Bob 應該測試負數 a -> True
異常
現在我們來討論一下如何處理異常的情況,并且這種處理既能遠離bug又能易于理解。
一個方法的標識(signature)包含它的名字、參數類型、返回類型,同時也包含該方法能觸發的異常。
參考閱讀: Exceptions in the Java Tutorials.
報告bug的異常
你可能已經在Java編程中遇到了一些異常,例如 ArrayIndex-OutOfBounds-Exception (數組訪問越界)或者 Null-Pointer-Exception (訪問一個null引用的對象)。這些異常通常都是用來報告你代碼里的bug ,同時它們報告的信息也能幫助你修復bug。
ArrayIndex-OutOfBounds- 和 Null-Pointer-Exception 大概是最常見的異常了,其他的例子有:
- ArithmeticException, 當發生計算錯誤時拋出,例如除0。
- NumberFormatException, 數字的類型不匹配的時候拋出,例如你向Integer.parseInt 傳入一個字符長而不是一個整數。
報告特殊結果的異常
異常不僅被用來報告bug,它們也被用來提升那些包含特殊結果的代碼的結構。
不幸的是,一個常見的處理特殊結果的方法就是返回一個特殊的值。你在Java庫中常常能發現這樣的設計:當你期望一個正整數的時候,特殊結果會返回一個-1;當你期望一個對象的時候,特殊結果會返回一個null 。這樣的方法如果謹慎使用也還OK,但是它有兩個問題。首先,它加重的檢查返回值的負擔。其次,程序員很可能會忘記檢查返回值(我們待會會看到通過使用異常,編譯器會幫助你處理這些問題)。
同時,找到一個“特殊值”返回并不是一件容易的事?,F在假設我們有一個 BirthdayBook 類,其中有一個lookup方法:
class BirthdayBook {LocalDate lookup(String name) { ... } }(LocalDate 是Java API的一個類.)
如果name在這個BirthdayBook中沒有入口,這個方法該如何返回呢?或許我們可以找一個永遠不會被人用到的日期。糟糕的程序員或許會選擇一個9/9/99,畢竟他們覺得沒有人會在這個世紀結束的時候使用這個程序。((事實上,它們錯了)
這里有一個更好的辦法,就是拋出一個異常:
LocalDate lookup(String name) throws NotFoundException {...if ( ...not found... )throw new NotFoundException();...調用者使用catch捕獲這個異常:
BirthdayBook birthdays = ... try {LocalDate birthdate = birthdays.lookup("Alyssa");// we know Alyssa's birthday } catch (NotFoundException nfe) {// her birthday was not in the birthday book }現在我們就不需要使用“特殊”的返回值來通報特殊情況了,調用者也不需要再檢查返回值。
閱讀小練習
1st birthday
假設我們在使用 BirthdayBook 中的 lookup 方法,它可能會拋出 NotFoundException.
如果“Elliot”不在birthdays里面(birthdays已經初始化了,并指向了一個對象),下面這些代碼會發生什么?
try {LocalDate birthdate = birthdays.lookup("Elliot"); }運行時報錯: NotFoundException
2nd birthday
try {LocalDate birthdate = birthdays.lookup("Elliot"); } catch (NotFoundException nfe) {birthdate = LocalDate.now(); }靜態錯誤: 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.)
運行時報錯: DateTimeException
已檢查(Checked)異常和未檢查(Unchecked)異常
我們已經看到了兩種不同目的的異常:報告特殊的結果或者報告bug。一個通用的規則是,我們用已檢查的異常來報告特殊結果,用未檢查的異常來報告bug。在后面一節中,我們會詳細介紹一些。
“ 已檢查 異常”這個名字是因為編譯器會檢查這種異常是否被正確處理:
- 如果一個方法拋出一個已檢查異常,這種可能性必須添加到它的標識中。例如 Not-Found-Exception就是一個已檢查異常,這也是為什么它的生命的結尾有一個 throws Not-Found-Exception.
- 如果一個方法調用一個可能拋出已檢查異常的方法,該方法要么處理它,要么在它的標識中說明該異常(交給它的調用者處理)。
所以如果你調用了 BirthdayBook中的 lookup 并忘記處理 Not-Found-Exception ,編譯器就會拒絕你的代碼。這非常有用,因為它確保了那些可能產生的特殊情況(異常)被處理。
相應的,未檢查異常用來報告bug。這些異常并不指望被代碼處理(除了一些頂層的代碼),同時這樣的異常也不應該被顯式拋出,例如邊界溢出、null值、非法參數、斷言失敗等等。同樣,編譯器不會檢查這些異常是否被 try-catch 處理或者用 throws 拋給上一層調用者。(Java允許你將未檢查的異常作為方法的標識,不過這沒有什么意義,我們也不建議這么做)
異常中有可能有和異常相關的信息。(如果構建體沒有提供,引用這個信息(String)的值將會是null )
Throwable 類層次
為了理解Java是如何定義一個異常是已檢查還是未檢查的,讓我們看一看Java異常類的層次圖:
Throwable 是一個能夠被拋出和捕獲的對象對應的類。Throwable的實現記錄了棧的結構(異常被拋出的時候),同時還有一個描述該異常的消息(可選)。任何被拋出或者捕獲的異常對象都應該是 Throwable的子類。
Error 是 Throwable 的一個子類,它被保留用于Java運行系統的異常,例如 StackOverflow-Error 和 OutOfMemory-Error.Errors應該被認為是不可恢復的,并且一般不會去捕獲它。(這里有一個特例, Assertion-Error 也是屬于Error 的,即使它反映的是用戶代碼錯誤)
下面描述了在Java中如何區別已檢查異常和未檢查異常:
- RuntimeException, Error, 以及它們的子類都是未檢查異常。編譯器不會要求它們被throws修飾,也不會要求它們被捕獲。
- 所有其他的throwables—— Throwable, Exception和其他子類都是已檢查異常。編譯器會要求它們被捕獲或者用throws傳給調用者處理。
當你定義你自己的異常時,你應該使它要么是 RuntimeException 的子類(未檢查異常),要么是 Exception 的子類(已檢查異常)。程序員通常不會生成 Error 或者 Throwable的子類,因為它們通常被Java保留使用。
閱讀小練習
Get to the point
假設我們寫了一個尋找兩點之間路徑的方法:
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 搜索的范圍是有限的(有邊界)。如果該方法沒有找到一個路徑,它就會拋出一個異常。
在設計方法時,以下哪一個異常是合理的?
- [ ] 已檢查異常 NoPathException
- [ ] 未檢查異常 NoPathException
- [x] 已檢查異常 PathNotFoundException
- [ ] 未檢查異常 PathNotFoundException
Don’t point that thing at me
當我們定義該異常時,應該使它是哪一個類的子類?
- [ ] Throwable
- [x] Exception
- [ ] Error
- [ ] RuntimeException
設計異常時應該考慮的事情
我們之前給了一個通用規則——對于特殊的結果(預測到的)使用已檢查異常,對于bug使用未檢查異常(意料之外)。這說得通,不過,在Java中異常并沒有這么“輕量化”。
除了對性能有影響,Java中的異常會帶來使用上的開銷:如果你要設計一個異常,你必須創建一個新的類。如果你調用一個可能拋出已檢查異常的方法,你必須使用 try-catch 處理它(即使你知道這個異常一定不會發生)。后一種情況導致了一個進退兩難的局面。例如,你設計了一個抽象隊列,你是應該期望使用者在循環pop的時候檢查隊列是否為空(作為前置條件),還是讓使用者自由的pop,最后拋出一個異常呢?如果你選擇拋出異常,那么即使使用者每次都檢查隊列不為空才pop,他還是要對這個異常進行處理。
所以我們提煉出另一個明確的規則:
- 對于意料之外的bug使用未檢查的異常,或者對于使用者來說避免異常產生的情況非常容易(例如檢查一個隊列是否為空)。
- 其他的情況我們使用已檢查異常。
這里舉出一些例子:
- 當隊列是空時,Queue.pop() 會拋出一個未檢查異常。因為檢查隊列是否為空對于用戶來說是容易的。(例如 Queue.size() or Queue.isEmpty().)
- 當無法連接互聯網時,Url.getWebPage() 拋出一個已檢查異常 IOException ,因為客戶可能無法確定調用的時候網絡是否好使。
- 當x沒有整數開方時,int integerSquareRoot(int x) 拋出一個已檢查異常 Not-Perfect-Square-Exception ,因為對于調用者來說,判斷一個整數是否為平方是困難的。
這些使用異常的“痛楚”也是很多Java API使用null引用或特殊值作為返回值的原因。額.....如果你嚴謹認真的使用這些返回值,這也不是什么糟糕的事情。
在規格說明中應該如何聲明異常
因為異常也可以歸為方法的輸出,所以我們應該在規格說明的后置條件中描述它。Java中是以 @throws 作為Javadoc中異常注釋的。Java也可能要求函數聲明時用throws標出可能拋出的異常 。這一節會討論什么時候使用這兩種方法。
對于非檢查的異常,由于它們描述的是意料之外的bug或者失敗,不屬于后置條件,所以不應該用 @throws 或 throws修飾它們。例如, NullPointerException就不應該在規格說明中列出——我們的前置條件已經隱式(顯式)的禁止了null值,這意味著如果使用者傳入一個null,我們可以沒有任何警告的扔出一個異常。例如下面這個規格說明,就沒有提到 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)而對于報告特殊結果的異常,我們應該在Javadoc中用 @throws 表示出來,并明確什么情況下會導致這個異常的拋出。另外,如果是一個已檢查異常,Java會要求在函數聲明的時候用 throws 標識出來。例如,假設 NotPerfectSquareException 是一個已檢查聲明:
/*** 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;對于報告特殊結果的未檢查異常,Java允許但是不要求使用 throws 在聲明中標識出。但是這種情況下通常不要使用 throws 因為這會使得閱讀者困惑(以為它是一個已檢查異常)。例如,假設你將EmptyQueueException定義為未檢查異常。那么你應該在Javadoc中使用 @throws對其進行說明,但是不要在函數聲明中將其標識出:
/*** 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();閱讀小練習
Throw all the things!
閱讀以下代碼并分析 Thing 對象:
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 是一個 已檢查 異常.
analyzeEverything可能會拋出哪一些異常?
[x] ArrayIndexOutOfBoundsException
[ ] IOException
[x] NullPointerException
[ ] AnalysisException
[ ] OutOfMemoryError
A terrible thing
如果 analyzeOneThing 自己會拋出一個 AnalysisException 異常,會發生什么?
[ ] 程序可能會崩潰
[x] 我們可能不能調用任何 analyzeOneThing
[ ] 我們可能會調用幾次 analyzeOneThing
總結
最后,再做一組練習看看你對今天學的內容理解的如何。
閱讀小練習
拼字游戲 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 中只有大寫字母
- [ ] crossings 中字母沒有重復
- [ ] scrabble 需要兩個參數
- [x] scrabble 返回字符串列表
scrabble的前置條件有哪些?
- [x] tiles 長度為 7
- [x] crossings 是一個大寫的字符串
- [x] scrabble參數的類型是 String 和 String
- [ ] scrabble 返回一個空的 ArrayList
拼字游戲 2
規格說明中的哪一部分是會被靜態檢查的?
- [ ] tiles 中只有大寫字母
- [ ] crossings 中字母沒有重復
- [ ] 當 tiles.length() != 7, scrabble 拋出 RuntimeException
- [x] scrabble 接收兩個參數
scrabble 的實現滿足了規格說明嗎?
- [ ] 是
- [ ] 否, 因為它會在無法獲取tiles長度時拋出 RuntimeException
- [x] 否,因為即使我們傳入一個可以組合成詞的tiles和crossings,它也會返回一個空列表。
一個規格說明就好像是實現者和使用者之間的防火墻。它使得分別開發成為可能:使用者可以在不理解源代碼的情況下使用模塊,實現者可以在不知道模塊如何被使用的情況下實現模塊。
現在讓我們想想今天的內容和我們三大目標之間的聯系:
- 遠離bug. 一個好的規格說明會清晰明確的要求實現者和使用者遵守相關的制約。而Bug經常是因為實現者和使用者對于接口的理解沖突導致的,規格說明會明顯的減小這種可能性。在模塊中使用一些能夠交由機器檢查的特性,例如靜態檢查、異常等而不是注釋會進一步降低bug的可能性。
- 易讀性. 一個簡潔準確的規格說明會比源代碼本身更易讀易懂。
- 可改動性. 規格說明在實現者和使用者之間建立了一個“契約”——只要這兩方遵守這份“契約”,他們可以對自己的代碼進行任何改變。
轉載于:https://www.cnblogs.com/liqiuhao/p/8566500.html
總結
以上是生活随笔為你收集整理的麻省理工18年春软件构造课程阅读06“规格说明”的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python运行是哪个键_python
- 下一篇: MySQL的my.cnf文件(解决5.7