[TOC]
程序开发原理的描述。
异常也是一种返回值!异常将业务代码和异常处理代码解耦。
以下的内容,取自程序开发原理:
过程(接口)抽象是从自变量到结果的映射,可能会有一些对自变量的修改。自变量属于过程的定义域(domain),结果属于过程的值域(range)。只有当自变量从属于过程的定义域的子集时,这个过程才是有意义的。例如,只有当其自变量是正数时计算阶乘的过程才有意义。再例如,只有当元素出现在数组中时,查找(search)过程才返回元素的索引。
处理这种情况的方法之一是使用局部过程(partial procedures),例如,只有当 gcd 的自变量是正数时,才可以进行定义:
public static int gcd(int n, int d){
// requires: n, d > 0
// effects: return the greatest common divisor of n and d
}
局部过程的调用者必须明确自变量属于定义域的子集,而实现者可以忽略这个子集之外的自变量。这样,在实现 gcd 的过程中,就可以忽略非正数自变量的情况了。
然而,一般来说,使用局部过程并不是什么好的解决方法,因为我们不能保证调用者一定不会传入 < 0 的值。局部过程不能保证程序的鲁棒性。一个稳健的程序,即使发生错误,也应该继续合理的变现出错误。如果发生错误,程序无法像无错误时一样表现,则必须以一个明确定义的方式表现。
增强稳健性的方法是使用全过程(total procedures):就是指为所有定义域内的输入都定义行为的过程。如果过程不能为其中某些输入执行其预期功能,则至少能够通知调用者。这样会引起调用者的注意,从而采取措施。
如果一个问题产生了,如何才能使调用者注意到它呢?一种可能,是使用特殊结果来传达这个信息。例如,如果计算阶乘过程的自变量不是正数,则会返回零:
public static int fact(int n)
// effects: if n > 0 return n! else return 0
这个解决方法其实并不大行,因为带有非法自变量的调用本身就是一个错误,如果用一个特殊的方法来处理这个情况就更好了。这样,使用这个接口的程序员就不太可能出错而忽略这个错误了。返回一个特殊值也可能给调用代码带来不便,返回了特殊值后,就必须再检查一下这个调用代码的结果,例如:
// 原来的代码
z = x + Num.fact(y);
// 优化后的代码
int r = Num.fact(y);
if(r > 0) z = x + r; else ...
另外,如果返回类型的每个值都是过程的一个可能的结果,则这种返回一个特殊结果的解决方法就是不可能的,因为没有剩余值可以使用了。例如,向量 vector 的 get 方法返回向量的第 i 个元素的值,这个值可以是任何一个对象或者是 null。所以我们无法通过返回一个特殊对象或者返回 null 来传达有关越界的索引信息。
我们需要的是一种即时在返回类型的每个值都是合法结果时也能传达所有情况中有关不寻常信息的途径。而且,这个途径最好能够以某种方法区别哪些情况,这样用户就不会因为出错而忽略了它们。如果这个途径能使对这些情况的处理与其他正常的程序控制流分开进行(将业务代码和异常处理代码解耦),则是最佳的。
异常机制机制提供了我们所需要的,允许过程通过返回一个结果而正常终止或者异常终止!
对可恢复的情况使用受检异常,对编程错误使用运行时异常。—— Effective Java
checked exception(受检异常) 继承自 Exception,unchecked exception(运行时异常,不可控异常) 继承自 RuntimeException。Exception 和 RuntimeException 都实现了 throwable 接口。
checked exception 必须被 try 或者抛出,unchecked exception 是隐藏的异常。
什么时候使用受检异常,什么时候使用运行时异常?如果期望调用者能够合理的恢复程序运行,对于这种情况就应该使用受检异常。
try{
String userInput = //read in user input
Long id = Long.parseLong(userInput);
}catch(NumberFormatException e){
id = 0; //recover the situation by setting the id to 0
}
用运行时异常来表明编程错误(调用接口的方式错误!)。大多数运行时异常都表示前提违例(precondition violation),就是指 API 的客户并没有遵守 API 规范建立的约定(你调用接口的方式错了,给你返回一个运行时异常,你重新调用)。例如,数组访问的预定指明了数组的下标值必须在 0 和数组长度 - 1之间。ArrayIndexOutOfBoundsException 表明违反了这个前提。
为什么 Web 服务返回的异常都使用 RuntimeException?因为 Web 服务的异常大多数都是接口调用方式不对,所以我们用 RuntimeException 来封装这些错误,并且直接冒泡返回,不需要层层 throw。
我们大多数时候使用的异常都是 RuntimeException。
异常应该用来去除大多数在 requires 中列出的约束条件(precondition)。requires 应该只为效率原因保留,或者在我们能确定调用该接口的时候一定能满足约束条件的时候,才保留 requires。
异常也应该用来消除在正常结果中的数字代码信息(异常就是异常,不能用特殊值来代替)。例如,如果元素不存在于数组中,search 就是一个异常,而不是返回一个特殊的数字。通过使用异常,我们能够清晰地区分正常的结果和异常的情况。
使用异常的规则:
- 如果使用的环境是局部的,例如调用一个 private 方法,则不需要使用异常,因为能证实 requires 格式很容易得到满足,并且可以恰当地使用特殊结果。
- 但是,如果使用的环境是非局部的,例如公开的接口,则应该使用异常来代替特殊的结果。并且,应该使用异常来代替使用 requires 格式。(参考 Web 接口的异常码机制)
带有 requires 格式的过程的实现,如果可能,应该检查这个 requires 格式是否满足。如果没有满足,就抛出运行时异常 FailureException(requires 是契约,既然没有按照契约调用该接口,就是编程错误,就应该抛出运行时异常)。
接口的方法名不应该列出 FailureException,而且过程的规格中也不应该提到抛出它。因为这个异常描述的是不符合 precondition 的情况,调用者只有满足了这个 precondition 才能不抛出该异常。
更普遍的情况是,每当代码检查到一个应该满足的 precondition,但是这个 precondition 不满足时,就应该抛出运行时异常,表示没有准确地调用这个接口!