14、第十五章异常

要想创建健壮的系统,它的每一个构件都必须是健壮的。

异常概念

C++的异常处理机制基于 Ada,Java 中的异常处理则建立在 C++的基础之上(尽管看上去更像 Object Pascal)。

基本异常

异常参数

所有标准异常类都有两个构造器:一个是无参构造器;另一个是接受字符串作为参数,以便能把相关信息放入异常对象的构造器。

Throwable 是异常类型的根类。

自定义异常

对异常来说,最重要的部分就是类名。

异常与记录日志

对于异常类来说,getMessage() 方法有点类似于 toString() 方法。

异常声明

不过还是有个能“作弊”的地方:可以声明方法将抛出异常,实际上却不抛出。编译器相信了这个声明,并强制此方法的用户像真的抛出异常那样使用这个方法。这样做的好处是,为异常先占个位子,以后就可以抛出这种异常而不用修改已有的代码。在定义抽象基类和接口时这种能力很重要,这样派生类或接口实现就能够抛出这些预先声明的异常。

捕获所有异常

多重捕获

通过 Java 7 的多重捕获机制,可以将不同类型的异常使用“或”将它们组合起来,只在一个 catch 块中使用:

try {
    x();
} catch (Except1 | Except2 | Except3 | Except4 e) {
    process();
}

栈轨迹

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

重新抛出异常

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

精准的重新抛出异常

Java 7 开始,允许抛出更具体的异常,即 catch 中捕捉的是父类,catch 块中可以抛出子类。

异常链

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

DynamicFieldsException dfe = new DynamicFieldsException();
dfe.initCause(new NullPointerException());
throw dfe;

Java 标准异常

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

异常的基本的概念是用名称代表发生的问题,并且异常的名称应该可以望文知意。异常并非全是在 java.lang 包里定义的;有些异常是用来支持其他像 util、net 和 io 这样的程序包,这些异常可以通过它们的完整名称或者从它们的父类中看出端倪。

特例:RuntimeException

RuntimeException 类型的异常(或者任何从 RuntimeException 继承的异常),也被称为“不受检查异常”。这种异常属于错误, 代表的是编程错误,将被自动捕获。

只能在代码中忽略 RuntimeException(及其子类)类型的异常,因为所有受检查类型异常的处理都是由编译器强制实施的。

异常限制

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

异常限制对构造器不起作用。

一个出现在基类方法的异常说明中的异常,不一定会出现在派生类方法的异常说明里。这点同继承的规则明显不同,在继承中,基类的方法必须出现在派生类里,换句话说,在继承和覆盖的过程中,某个特定方法的“异常说明的接口”不是变大了而是变小了——这恰好和类接口在继承时的情形相反。

Try-With-Resources 用法

Java 7 引入了 try-with-resources 语法。

try-with-resources 定义子句中创建的对象(在括号内)必须实现 java.lang.Autocloseable 接口,这个接口有一个方法,close()。当在 Java 7 中引入 AutoCloseable 时,许多接口和类被修改以实现它;查看 Javadocs 中的 AutoCloseable,可以找到所有实现该接口的类列表,其中包括 Stream 对象。

规范头中定义的每个对象都会在 try 语句块运行结束之后调用 close() 方法。

Java 5 中的 Closeable 已经被修改,修改之后的接口继承了 AutoCloseable 接口。所以所有实现了 Closeable 接口的对象,都支持了 try-with-resources 特性。

不能在资源规范头中定义了一个不是 AutoCloseable 的对象。

异常匹配

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

查找的时候并不要求抛出的异常同处理程序所声明的异常完全匹配。派生类的对象也可以匹配其基类的处理程序。

其他可选方式

异常处理的一个重要原则是“只有在你知道如何处理的情况下才捕获异常"。

所有模型都是错误的,但有些是能用的。

好的程序设计语言能帮助程序员写出好程序,但无论哪种语言都避免不了程序员用它写出了坏程序。

异常指南

应该在下列情况下使用异常:

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