Java的基本理念是,“结构不佳的代码不能运行”
—— Java编程思想
什么时候使用异常
发现错误的理想时机是在编译阶段,也就是试图运行程序之前。然而,编译阶段并不能找出所有的错误,余下的问题必须在运行期间解决。这就需要错误源能够通过某种方式,把适当的信息传递给某个接收者——该接收者将知道如何正确处理这个问题。
举个例子,当一个方法遇到一种情况,这种情况下,它不能满足约定,这时应该如何处理?
传统的做法是方法应该返回某种错误码。调用者被迫区检查错误,如果调用者也不能处理错误,那就给调用者的调用者返回一个错误码……
程序员并不总是检查和传递返回的错误码,结果错误没有被检测到,导致后面的严重破坏!
C以及其他早期语言常常具有多种错误处理模式,这些模式往往建立在约定俗成的基础之上,而并不属于语言的一部分。
只针对异常的情况才使用异常
从一个反面教材开始:
try {
int i = 0;
while (true)
range[i++].climb();
} catch (ArrayIndexOutOfBoundsException e) {
}
上面这种情况就是乱用异常的典型,它的构想是非常拙劣的。当这个循环企图访问数组边界之外的第一个数组元素时,用throw、catch和忽略ArrayIndexOutOfBoundsException的手段来达到终止无限循环的目的,对于任何一个合格的程序员来说,它的实现用下面的标准模式一看便知:
for (Mountain m : range)
m.climb();
异常机制的设计初衷是用于不正常的情形。所以很少会有JVM实现试图对它们进行优化,使得异常与显示的测试一样快速,例如for等。把代码放到try-catch块中,反而阻止了JVM实现本来可能要执行的某些特定优化。这个例子说明:
异常应该只用于异常的情况下,他们永远不应该用于正常的控制流;
设计良好的API不应该强迫它为了正常的控制流而使用异常;
Java的异常架构
所有的异常类都派生自Throwable。当发生来某种异常,而这种异常不是期望应用程序处理的,比如内存耗尽等,则JVM会抛出Error。这种异常不推荐程序员使用。
一般程序员使用的异常属于Exception类的异常,他们分为两种:
未检查异常(unchecked exception),属于RuntimeException的子类,它属于不需要也不应该被捕获的可抛出结构。用运行时异常来表明编程错误。大多数的运行时异常都表示前提违例(precondition violation),意思是API的客户没有遵守API的规范建立的约定,比如要求参数不能为Null,但还是传递Null参数,实现的所有未检查异常(未受检异常)都应该是RuntimeException的子类;
已检查异常(checked exception),属于Exception的子类,也称为受检异常,如果期望调用者能够适当地恢复,对于这种情况就应该使用它。通过抛出异常,强迫调用者在一个catch里处理该异常,或将它传播出去,尤其在设计API时,设计者让API用户面对受检的异常;
异常也是个对象,可以在它上面定义任意的方法。这些方法的主要用途是为捕获异常的代码提供额外的信息,特别是关于引发这个异常条件的信息。
避免不必要的使用受检的异常
受检异常强迫使用该API的用户必须catch异常,大大增加来程序的可靠性。但是多度使用会使API使用起来非常不方便。用户必须声明它抛出的这些异常,或者让它们传播出去,无论哪种方法,都给API用户增添来负担。
那什么时候使用受检异常比较合适呢?
如果,正确的使用了API,但并不能组织这种异常条件的发生,并且一旦产生异常,使用API的用户会立即采取有用的动作,这种情况就是使用受检异常的典型场景。
例如,API的设计人员可以尝试着问自己:API用户将如何处理该异常,会有比下面代码更好的方式吗?
catch (CheckedException e) {
throw new AssertionError();
}
下面这种做法如何?
catch (CheckedException e) {
e.printStackTrace();
System.exit(1);
}
如果使用API的用户无法做的比这更好,那还是使用未受检异常更为合适。
这段代码的意思为,如果这个异常发生了,程序注定会停止,或者要完成的事情注定不可以完成,那直接抛出未受检异常让程序中断,暴露出问题更为合适。
例如,在缴费电话费时,由于余额不足导致缴费失败,可以抛出受检异常,并提供一个方法,查询所需的金额等。
优先使用标准的异常
代码重用是值得提倡的,这是一条通用的规则,异常也不例外。
Java提供来一组基本的未受检异常,它们满足了绝大多数API的异常需求。
异常 | 场合 |
IllegalArgumentException | 非null的参数值不正确 |
IllegalStateException | 对于方法调用而言,对象状态不合适 |
NullPointerException | 在禁止使用null的情况下参数值为null |
IndexOutOfBoundsException | 下标参数值越界 |
ConcurrentModificationException | 进行并发修改的情况下,检测到并发修改对象 |
UnsupportedOperationException | 对象不支持用户请求的方法 |
抛出与抽象相对应的异常
如果方法抛出的异常与它所执行的任务没有明显联系,这种情形将会使人不知所措。
更高层的实现应该捕获底层的异常,同时抛出可以按照高层抽象进行解释的异常。
如下所示:
try {
} catch (LowerLevelException e) {
throw new HigherLevelException(...);
}
或者:
Iterator i = ...
try {
return i.next()
} catch (NoSuchElementException e) {
throw new IndexOutOfBoundsException("Index: " + index);
}
还有一种特殊的异常转义形式,称为异常链(exception chaining),如果低层的异常对于调试导致高层异常的问题非常有帮助,使用异常链就很合适。底层异常的原因可以传给高层异常,高层异常提供访问方法,来获得底层异常:
try {
} catch (LowerLevelException cause) {
throw new HigherLevelException(cause);
}
尽管异常转移与不佳选择地从底层传递异常的做法相比有所改进,但是它也不能被滥用。
如果可能,处理来自跌穿那个异常的最好做法是,在调用底层方法之前,前确保他们会执行成功,从而避免它们抛出异常。
努力保持失败的原子性
当对象抛出异常后,通常期望这个对象仍然保持在一种定义良好的可用状态之中,即使失败是发生在执行某个操作过程中间,对于受检异常而言,这点尤为重要,因为API用户期望能从这种异常中恢复。
一般而言,失败的方法调用应该使对象保持在被调用之前的状态。具有这种属性的方法被称为具有失败原子性(failure atomic)。
有几种方法可以实现这种效果:
最简单的办法是设计一个不可变对象;
设计处理过程的顺序,使得任何可能会失败的部分都在对象状态改变之前发生;
编写一段恢复代码(recovery code);
在对象的一份临时拷贝上执行操作;
异常文档的重要性
始终要单独声明受检的异常,并且利用JavaDoc的@throws标记,准确地记录下抛出每个异常的条件。
永远不要throws Exception,或者throws Throwable,它们会掩盖该方法抛出的任何其他异常。
未受检的异常通常代表编程上的错误,让API用户了解所有这些错误都有助于帮助他们避免调用错误。对于方法可能抛出的未受检异常,如果将这些异常信息很好的组织成列表文档,就可以有效地描述出这个方法被成功执行的前提条件(precondition)。
对于接口中的方法,在文档中记录下它可能抛出的未受检异常显得尤为重要。这份文档构成了该接口的通用约定(general contract)的一部分,它指定来该接口的多个实现必须遵守的公共行为。
使用JavaDoc的@throws标签记录下一个方法可能抛出的每个未受检异常,但是不要使用throws关键字将未受检的异常包含在方法的声明中。
如果一个类中的许多方法处于同样的原因抛出同一个异常,在类的文档注视中,堆这个异常建立文档。
不要忽略异常
空的catch达不到应有的目的,至少,catch块也应该包含一条说明,解释为什么忽略这个异常。
希望不要忽略异常。
附录A
java.lang.RuntimeException的直接子类有这些:
- AnnotationTypeMismatchException
- ArithmeticException
- ArrayStoreException
- BufferOverflowException
- BufferUnderflowException
- CannotRedoException
- CannotUndoException
- ClassCastException
- CMMException
- ConcurrentModificationException
- DataBindingException
- DOMException
- EmptyStackException
- EnumConstantNotPresentException
- EventException
- IllegalArgumentException
- IllegalMonitorStateException
- IllegalPathStateException
- IllegalStateException
- ImagingOpException
- IncompleteAnnotationException
- IndexOutOfBoundsException
- JMRuntimeException
- LSException
- MalformedParameterizedTypeException
- MirroredTypeException
- MirroredTypesException
- MissingResourceException
- NegativeArraySizeException
- NoSuchElementException
- NoSuchMechanismException
- NullPointerException
- ProfileDataException
- ProviderException
- RasterFormatException
- RejectedExecutionException
- SecurityException
- SystemException
- TypeConstraintException
- TypeNotPresentException
- UndeclaredThrowableException
- UnknownAnnotationValueException
- UnknownElementException
- UnknownTypeException
- UnmodifiableSetException
- UnsupportedOperationException
- WebServiceException