Java的基本理念是,“结构不佳的代码不能运行”java
—— Java编程思想程序员
发现错误的理想时机是在编译阶段,也就是试图运行程序以前。然而,编译阶段并不能找出全部的错误,余下的问题必须在运行期间解决。这就须要错误源可以经过某种方式,把适当的信息传递给某个接收者——该接收者将知道如何正确处理这个问题。编程
举个例子,当一个方法遇到一种状况,这种状况下,它不能知足约定,这时应该如何处理?数组
传统的作法是方法应该返回某种错误码。调用者被迫区检查错误,若是调用者也不能处理错误,那就给调用者的调用者返回一个错误码……架构
程序员并不老是检查和传递返回的错误码,结果错误没有被检测到,致使后面的严重破坏!并发
C以及其余早期语言经常具备多种错误处理模式,这些模式每每创建在约定俗成的基础之上,而并不属于语言的一部分。ide
从一个反面教材开始:测试
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实现原本可能要执行的某些特定优化。这个例子说明:atom
异常应该只用于异常的状况下,他们永远不该该用于正常的控制流;
设计良好的API不该该强迫它为了正常的控制流而使用异常;
全部的异常类都派生自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块也应该包含一条说明,解释为何忽略这个异常。
但愿不要忽略异常。
java.lang.RuntimeException的直接子类有这些: