一、概念

通过异常:往往能够降低错误处理代码的复杂度。

如果使用异常,就不必在方法调用处进行检查,因为异常机制将保证能捕获这个错误。并且只需在一个地方处理错误,即异常处理程序中。

二、基本异常

异常情形是指阻止当前方法或作用域继续执行的问题。把异常情形与普通问题相区分很重要,所谓的普通问题是指,在当前环境下能得到足够的信息,总能处理这个错误。

当抛出异常后,有几件事会随之发生。同Java其他对象被创建一样,将使用new在堆上创建异常对象。然后的执行路径(不能继续执行下去)被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。这个恰当的地方就是“异常处理程序(exception handler)”,它的任务是将程序从错误状态中恢复:以使程序能要么换一种方式运行,要么继续运行下去。

异常使得我们可以将每件事都当做一个事务来考虑,而异常可以看护着这些事务的底线。我们还可以将异常看作是一种内建的恢复(undo)系统,因为(在细心使用的情况下)我们在程序中可以拥有各种不同的恢复点。

异常参数

所有标准异常类都有两个构造器:一个是默认构造器;另一个是接受字符串作为参数。

三、捕获异常

1、try块

2、异常处理程序

当异常被抛出时,异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序。

终止与恢复

Java支持终止模型(Java和C++所支持的模型)。一旦异常被抛出,就表明错误已无法挽回,也不能回来继续执行。

另一种称为恢复模型。异常处理程序的工作是修正错误,然后重新尝试调用出问题的方法,并认为第二次能成功。不很实用,主要原因可能是它导致的耦合:恢复性的处理程序需要了解异常抛出的地点,势必要包含依赖于抛出位置的非通用性代码。

四、创建自定义异常

要自己定义异常类,必须从已有的异常类继承,最好是选择意思相近的异常类继承。对异常来说,最重要的部分就是类型的名称。

注意:System.err是打印到控制台的标准错误流。

在异常处理程序中,调用了在Throwable类声明(Exception即从此类继承)的printStackTrace()方法。它将打印“从方法调用处直到异常抛出处”的方法调用序列。在缺省情况下,信息将被输出到标准错误流,但你也可以使用重载的版本,把信息输出到任意的流中。

异常与记录文件

我们需要捕获和记录其他人编写的异常,因此我们必须在异常处理程序中生成日志消息。

书中使用了Logger对象,可以使用log4j来记录日志。

五、异常说明

Java鼓励把方法可能会抛出的异常类型,告知使用此方法的程序员。这是种优雅的做法,它使得调用者能确切知道写什么样的代码可以捕获所有潜在的异常。Java提供了相应的语法(并强制你使用这个语法),使你能以礼貌的方式告知客户端程序员某个方法可能会抛出的异常类型,然后客户端程序员就可以进行相应的处理。这就是“异常说明”(exception specification),它属于方法声明的一部分,紧跟在形式参数列表之后。

异常说明使用了附加的关键字throws,后面接一个所有潜在异常类型的列表。

方法里的代码产生了异常却没有进行处理,编译器会发现这个问题并提醒你:要么处理这个异常,要么就在异常说明中表明此方法将产生异常。通过这种自顶向下强制执行的异常说明机制,Java在编译期就可以保证相当程度的异常一致性。

你可以声明方法将抛出异常,实际上却不抛出。这样做的好处是,为异常先占了个位子,以后就可以抛出这种异常而不用修改已有的代码。在定义抽像基类和接口时这种能力很重要,这样派生类或接口实现就能够抛出这些预先声明的异常。

这种在编译期被强制检查的异常称为“被检查的异常”(checked exception)。

六、捕获所有异常

  1. void printStackTrace()
  2. void printStackTrace(PrintStream)
  3. void printStackTrace(java.io.PrintWriter)

打印ThrowableThrowable的调用栈轨迹(call stack trace)。

  1. Throwable fillInStackTrace( )

用于在Throwable对象的内部记录栈框架(stack frame)的当前状态。这在程序重新抛出错误或异常时很有用。

1、栈轨迹

printStackTrace()方法所提供的信息可以通过getStackTrace()方法直接访问,这个方法将返回一个由栈轨迹中的元素所构成的数组,其中每一个元素都表示栈中的一帧。元素0是栈顶元素,并且是调用序列中的最后一个方法调用(这个Throwable被创建和抛出之处)。

2、重新抛出异常

重抛异常会把异常抛给上一级环境中的异常处理程序,同一个try块的后续catch子句将被忽略。

printStackTrace()方法显示的是原来异常抛出点的调用栈信息,而并非重新抛出点的信息。要想更新这个信息,可以调用fillInStackTrace()方法,这将返回一个Throwable对象,它是通过把当前调用栈信息填入原来那个异常对象而建立的。

3、异常链

常常会想要在捕获一个异常然后抛出另一个异常,并且希望把原始异常的信息保存下来,这被称为“异常链”(Exception chaining)。所有Throwable的子类在构造器中都可以接受一个Cause对象作为参数。这个Cause就用来表示原始异常,这样通过把原始异常传递给新的异常,使得即使你在当前位置创建并抛出了新的异常,你也能通过这个异常链追踪到异常最初发生的位置。

Throwable的子类中,只有三种基本的异常类提供了带Cause的构造器。它们是Error(用于Java虚拟机报告系统错误)、Exception,以及RuntimeException。如果你要把其它类型的异常链接起来,你应该使用initCause()方法而不是构造器。

  1. DynamicFieldsException dfe = new DynamicFieldsException();
  2. dfe.initCause(new NullPointerException());

七、Java标准异常

Throwable这个Java类被用来表示任何可以作为异常被抛出的类。Throwable对象可分为两种类型(指从Throwable继承而得到的类型):Error用来表示你不用关心的编译期和系统错误(除了特殊情况); Exception是可以被抛出的基本类型,在Java类库、用户方法以及运行时故障中都可能抛出Exception型异常。所以Java程序员关心的主要是Exception

异常的基本的概念是用名称代表发生的问题,并且异常的名称应该可以望文知意。

特例:RuntimeException

属于运行期异常的类型有很多。它们会自动被Java虚拟机抛出,所以你不必在异常说明中把它们列出来。这些异常都是从RuntimeException类继承而来,所以既体现了继承的优点,使用起来也很方便。这构成了一组具有相同特征和行为的异常类型。并且,你也不再需要在异常说明中声明方法将抛出RuntimeException类型的异常(或者任何从RuntimeException继承的异常),它们也被称为“未被检查的异常”(unchecked exception)。这种异常属于错误,将被自动捕获。要是你自己去检查RuntimeException的话,代码就显得太混乱了。不过尽管你通常不用捕获RuntimeException异常,但还是可以在代码中抛出RuntimeException类型的异常。

RuntimeException代表的是编程错误:

  1. 无法预料的错误。比如从你控制范围之外传递进来的null引用。
  2. 作为程序员,你应该在代码中进行检查的错误。

(比如对于ArrayIndexOutOfBoundsException,你就得注意一下数组的大小了)在一个地方发生的异常,常常会在另一个地方导致错误。

八、使用finally进行清理

1、finally用来做什么

当你要把除内存之外的资源恢复到它们的初始状态时,就要用到finally子句。这种需要清理的资源包括:已经打开的文件或网络连接,你在屏幕上画的图形,甚至可以是外部世界的某个开关。

当涉及到breakcontinue语句的时候,finally子句也会得到执行。请注意,如果把finally子句和带标记的breakcontinue配合使用,在Java里就没必要使用goto语句了。

2、return中使用finally

因为finally子句总是会执行,所以在一个方法中,可以有多个点返回,并且可以保证重要的清理工作仍然会执行。

3、缺憾:异常丢失

抛出的异常被finally子句里的异常所取代,另外一种更加简单的丢失异常的方式是从finally子句中返回。

前一个异常还没处理就抛出下一个异常。(C++中看作为糟糕的编程错误)

异常可能以一种微妙和难以察觉的方式完全丢失。

九、异常的限制

当覆盖方法的时候,只能抛出在基类方法的异常说明里列出的那些异常。

如果在扩展类的同时又实现了接口,那么接口的event()方法就不能改变在基类中的event()方法的异常接口。否则的话,使用基类的时候就不能判断是否捕获了正确的异常。

  1. interface Storm{
  2. public void event() throws RainedOut;
  3. }
  4. class Inning{
  5. public void event() throws BaseException;
  6. }
  7. class StormInning extends Inning implements RainedOut{
  8. public void event() /*throws RainedOut*/{
  9. }
  10. }

异常限制对构造器不起作用。派生类构造器的异常说明必须包含基类构造器的异常说明。

派生类方法可以不抛出任何异常,即使它是基类所定义的异常。

方法类型是由方法的名字与参数的类型组成的。因此,不能基于异常说明重载方法。

十、构造器

Java的缺陷:除了内存的清理外,所有的清理都不会自动发生。

对于在构造器阶段可能会抛出异常,并且要求清理的类,最安全的方式是使用嵌套的try子句。

应该创建不能失败的构造器。

十一、异常匹配

异常处理处理系统会按照代码的书写顺序找到“最近”的处理程序。找到匹配的处理程序之后,它就认为异常将得到处理,然后就不再查找。

十二、其他可选方式

开发异常处理程序的初衷是方便处理异常。

只有在知道如何处理的情况下才捕获异常。异常处理的一个重要目标是把错误处理的代码同错误发生的地点相分离。

“被检查的异常”强制你在可能还没有准备好处理错误的时候被迫加上catch子句,导致了吞食则有害(harmful if swallowed)的问题。

  1. try{
  2. }catch(ObligatoryExecption e){}//gulp

1、历史

C++的异常设计参考了CLU的方式,C++的设计者Stroustrup声称其目标是减少恢复错误所需要的代码。(作者认为是对那些通常情况下不写C的错误处理的程序员听的。写C程序的习惯是,忽略所有错误,然后通过调试来跟踪错误)同时带来的另一种思想:异常说明。这样可以用编程的方法在方法的特征签名中,声明这个方法将会抛出异常。

C++异常说明不属于函数的类型信息。

2、观点

Java无谓地发明了“被检查的异常”。(受C++异常说明启发,且C++程序员往往对此无动于衷的事实所影响)

对于小程序,“被检查的异常”的好处很明显。

减少编译时施加的约束能显著提高编程效率。反射和泛型是用来补偿静态类型检查所到来的过多限制。

3、把异常传递给控制台

  1. public static void main(String[] args) throws Exception{}

4、把“被检查的异常”转换为“不检查的异常”

直接把“被检查的异常”包进RuntimeException里面。

  1. Try{
  2. }catch(IDontKnowWhatToDoWithThisExcettion e){
  3. throw new RuntimeException(e);
  4. }

也可以创建自己的RuntimeException的子类。

十三、异常使用指南

  1. 在恰当的级别处理异常。(知道该如何处理的情况才捕获异常)
  2. 解决问题并且重新调用产生异常的方法。
  3. 进行少许修补,然后绕开异常发生的地方继续执行;
  4. 用别的数据进行计算,以代替方法预计会返回的值;
  5. 把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层;
  6. 把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层;
  7. 终止程序;
  8. 进行简化。(如果你的异常模式使问题变得复杂,那用起来会非常痛苦也很烦人)
  9. 让类库和程序更安全。(这既是为调式做短期投资,也是在为程序的健壮性做长期投资)