<font size="3">html
本文内容来自MIT_6.031_sp18: Software Construction课程的Readings部分,采用CC BY-SA 4.0协议。java
因为咱们学校(哈工大)大二软件构造课程的大部分素材取自此,也是推荐的阅读材料之一,因而打算作一些翻译工做,本身学习的同时也能帮到一些懒得看英文的朋友。另外,该课程的阅读资料中有许多练习题,可是没有标准答案,所给出的答案均为译者所写,有错误的地方还请指出。git
<br />程序员
<br />github
译者:李秋豪web
审校:编程
V1.0 Tue Mar 13 22:17:35 CST 2018api
<br />数组
规格说明是团队合做中的关键点。若是没有规格说明,就没有办法分工实现各类方法。规格说明就像一份合同:实现者的义务在于知足合同的要求,客户能够依赖这些要求工做。事实上,咱们会发现就像真的合同同样,规格说明对双方都有制约:当合同上有前置条件时,客户有责任知足这些条件。安全
在这篇阅读材料中咱们会研究方法中的规格说明,讨论前置条件和后置条件分别是什么,它们对方法的实现者和使用者来讲意味着什么。咱们也会讨论如何使用异常——Java、Python、以及不少现代语言中的一个重要特性,它使得方法的接口更加安全也更加易懂。
<br />
在编程中,不少让人抓狂的bug是因为两个地方的代码对于接口行为的理解不同。虽然每个程序员在内心都有一份“规格说明”,可是不是全部程序员都会把他们写下来。最终,一个团队中的不一样程序员对于同一个接口就有不一样的“规格说明”了。当程序崩溃的时候,就很难发现问题在哪里。简洁准确的的规格说明使得咱们远离bug,更能够快速发现问题所在。
规格说明对使用者(客户)来讲也是颇有用的,它们使得使用者没必要去阅读源码。若是你还不相信阅读规格说明比阅读源码更简单易懂的话,看看下面这个标准的Java规格说明和它对应的源码,它是 BigInteger
中的一个方法:
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
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
的构造体, compareMagnitude
, subtract
以及trustedStripLeadingZeroInts
的实现——而这还仅仅只是开始。
另外,规格说明对于实现者也是颇有好处的,由于它们给了实现者更改实现策略而不告诉使用者的自由。同时,规格说明能够限定一些特殊的输入,这样实现者就能够省略一些麻烦的检查和处理,代码也能够运行的更快。
如上图所示,规格说明就好像一道防火墙同样将客户和实现者隔离开。它使得客户没必要知道这个单元是如何运行的(没必要阅读源码),也使得实现者没必要管这个单元会被怎么使用(由于客户要遵照前置条件)。这种隔离形成了“解耦”(decoupling),客户本身的代码和实现者的代码能够独立发生改动,只要双方都遵循规格说明对应的制约。
<br />
思考下面两个方法的异同:
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
<br />
一个规格说明含有如下两个“条款”:
其中前置条件是客户的义务(谁调用的这个方法)。它确保了方法被调用时所处的状态。
然后置条件是实现者的义务。若是前置条件获得了知足,那么该方法的行为应该符合后置条件的要求,例如返回一个合适的值,抛出一个特定的异常,修改一个特定的对象等等。
若是前置条件不知足的话,实现也不须要知足后置条件——方法能够作任何事情,例如不终止而是抛出一个异常、返回一个任意的值、作一个任意的修改等等。
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
为空的时候,为何要抛出一个异常?
有一些语言(例如 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中哪一行是有问题的?
/*
* 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
*/
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.
在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()
会产生一个NullPointerException
.
另外要注意一点,非原始类型的聚合类型例如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。
<br />
在测试中,咱们谈到了黑盒测试意味着仅仅经过规格说明构建测试,而白盒测试是经过代码实现来构建测试(译者注:阅读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
的时候,就极可能产生意想不到的错误。
<br />
咱们在以前的阅读材料中谈到了可改变的对象 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)是不被容许的,除非显式的声明 。例如 toLowerCase
的规格说明中就没有谈到该方法会不会改变参数对象(不会改变),而sort
中就显式的说明了。
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
<br />
如今咱们来讨论一下如何处理异常的状况,而且这种处理既能远离bug又能易于理解。
一个方法的标识(signature)包含它的名字、参数类型、返回类型,同时也包含该方法能触发的异常。
你可能已经在Java编程中遇到了一些异常,例如 ArrayIndexOutOfBoundsException
(数组访问越界)或者 NullPointerException
(访问一个null
引用的对象)。这些异常一般都是用来报告你代码里的bug ,同时它们报告的信息也能帮助你修复bug。
ArrayIndexOutOfBounds-
和 NullPointerException
大概是最多见的异常了,其余的例子有:
ArithmeticException
, 当发生计算错误时抛出,例如除0。NumberFormatException
, 数字的类型不匹配的时候抛出,例如你向Integer.parseInt
传入一个字符长而不是一个整数。异常不只被用来报告bug,它们也被用来提高那些包含特殊结果的代码的结构。
不幸的是,一个常见的处理特殊结果的方法就是返回一个特殊的值。你在Java库中经常能发现这样的设计:当你指望一个正整数的时候,特殊结果会返回一个-1;当你指望一个对象的时候,特殊结果会返回一个null
。这样的方法若是谨慎使用也还OK,可是它有两个问题。首先,它加剧的检查返回值的负担。其次,程序员极可能会忘记检查返回值(咱们待会会看到经过使用异常,编译器会帮助你处理这些问题)。
同时,找到一个“特殊值”返回并非一件容易的事。如今假设咱们有一个 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
<br />
咱们已经看到了两种不一样目的的异常:报告特殊的结果或者报告bug。一个通用的规则是,咱们用已检查的异常来报告特殊结果,用未检查的异常来报告bug。在后面一节中,咱们会详细介绍一些。
“ 已检查 异常”这个名字是由于编译器会检查这种异常是否被正确处理:
NotFoundException
就是一个已检查异常,这也是为何它的生命的结尾有一个 throws NotFoundException
.因此若是你调用了 BirthdayBook
中的 lookup
并忘记处理 NotFoundException
,编译器就会拒绝你的代码。这很是有用,由于它确保了那些可能产生的特殊状况(异常)被处理。
相应的,未检查异经常使用来报告bug。这些异常并不期望被代码处理(除了一些顶层的代码),同时这样的异常也不该该被显式抛出,例如边界溢出、null值、非法参数、断言失败等等。一样,编译器不会检查这些异常是否被 try
-catch
处理或者用 throws
抛给上一层调用者。(Java容许你将未检查的异常做为方法的标识,不过这没有什么意义,咱们也不建议这么作)
异常中有可能有和异常相关的信息。(若是构建体没有提供,引用这个信息(String)的值将会是null
)
为了理解Java是如何定义一个异常是已检查仍是未检查的,让咱们看一看Java异常类的层次图:
Throwable
是一个可以被抛出和捕获的对象对应的类。Throwable
的实现记录了栈的结构(异常被抛出的时候),同时还有一个描述该异常的消息(可选)。任何被抛出或者捕获的异常对象都应该是 Throwable
的子类。
Error
是 Throwable
的一个子类,它被保留用于Java运行系统的异常,例如 StackOverflowError
和 OutOfMemoryError
.Errors应该被认为是不可恢复的,而且通常不会去捕获它。(这里有一个特例, AssertionError
也是属于Error
的,即便它反映的是用户代码错误)
下面描述了在Java中如何区别已检查异常和未检查异常:
RuntimeException
, Error
, 以及它们的子类都是未检查异常。编译器不会要求它们被throws
修饰,也不会要求它们被捕获。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
PathNotFoundException
PathNotFoundException
Don’t point that thing at me
当咱们定义该异常时,应该使它是哪个类的子类?
Throwable
Exception
Error
RuntimeException
<br />
咱们以前给了一个通用规则——对于特殊的结果(预测到的)使用已检查异常,对于bug使用未检查异常(意料以外)。这说得通,不过,在Java中异常并无这么“轻量化”。
除了对性能有影响,Java中的异常会带来使用上的开销:若是你要设计一个异常,你必须建立一个新的类。若是你调用一个可能抛出已检查异常的方法,你必须使用 try
-catch
处理它(即便你知道这个异常必定不会发生)。后一种状况致使了一个进退两难的局面。例如,你设计了一个抽象队列,你是应该指望使用者在循环pop的时候检查队列是否为空(做为前置条件),仍是让使用者自由的pop,最后抛出一个异常呢?若是你选择抛出异常,那么即便使用者每次都检查队列不为空才pop,他仍是要对这个异常进行处理。
因此咱们提炼出另外一个明确的规则:
这里举出一些例子:
Queue.pop()
会抛出一个未检查异常。由于检查队列是否为空对于用户来讲是容易的。(例如 Queue.size()
or Queue.isEmpty()
.)Url.getWebPage()
抛出一个已检查异常 IOException
,由于客户可能没法肯定调用的时候网络是否好使。x
没有整数开方时,int integerSquareRoot(int x)
抛出一个已检查异常 NotPerfectSquareException
,由于对于调用者来讲,判断一个整数是否为平方是困难的。这些使用异常的“痛楚”也是不少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
<br />
最后,再作一组练习看看你对今天学的内容理解的如何。
拼字游戏 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 taking letters 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
须要两个参数scrabble
返回字符串列表scrabble
的前置条件有哪些?
tiles
长度为 7crossings
是一个大写的字符串scrabble
参数的类型是 String
和 String
scrabble
返回一个空的 ArrayList
拼字游戏 2
规格说明中的哪一部分是会被静态检查的?
tiles
中只有大写字母crossings
中字母没有重复tiles.length() != 7
, scrabble
抛出 RuntimeException
scrabble
接收两个参数scrabble
的实现知足了规格说明吗?
tiles
长度时抛出 RuntimeException
一个规格说明就好像是实现者和使用者之间的防火墙。它使得分别开发成为可能:使用者能够在不理解源代码的状况下使用模块,实现者能够在不知道模块如何被使用的状况下实现模块。
如今让咱们想一想今天的内容和咱们三大目标之间的联系:
</font>