【Java编程思想】12.通过异常处理错误

Java 的基本理念是“结构不佳的代码不能运行”。
异常处理是 Java 中唯一正式的错误报告机制,并且通过编译器强制执行。


12.1 概念

异常机制会保证能够捕获错误,并且只需在一个地方(即异常处理程序中)处理错即可。


12.2 基本异常

异常情形(exceptional condition)是指组织当前方法或作用域继续执行的问题。当前环境遭遇异常情形时,表示程序不能继续下去,因为在当前环境下无法获得必要的信息来解决问题,能做的就是从当前环境下跳出,并且将问题提交给上一级环境。--这就是抛出异常的场景了。
在抛出异常之后,

  • 首先 Java 会使用 new 关键字在堆上创建一场对象
  • 然后当前的执行路径会被终止,并且从当前环境中弹出对异常对象的引用。
  • 最后异常处理机制接管程序,并且开始寻找一个恰当的地方来继续执行程序。

这个恰当的地方就是异常处理程序,它的任务是将程序从错误状态中恢复,以使程序要么换一种方式运行,要么继续运行下去。

标准异常类都有两个构造器:一个是默认构造器,另一个接收字符串作为参数,以便能把相关信息放入异常对象的构造器。
在使用 new 创建了异常对象之后,此对象的引用将传给 throw。可以用抛出异常的方式从当前的作用域退出。这两种情况下都将会返回一个一场对象,然后退出方法或作用域。
虽然看起来抛出异常好像跟“方法返回”类似,但是,异常返回的“地点”和普通方法调用返回的“地点”完全不同。(异常会在一个恰当的异常处理程序中得到解决,位置离被抛出的地方很远,可能会跨越方法调用栈的很多层次。)

能抛出任意类型的 Throwable 类是异常类型的根类。


12.3 捕获异常

一般来说,方法内部或者方法内部调用抛出异常时,这个方法会在抛出异常的过程中结束。

try {
    // code that might generate exception
} catch {
    // handle exception
}

使用上述结构,可以在 try 内部代码抛出异常的时候,捕获到异常,并对异常进行处理。try 内的部分可以称之为监控区域(guared region)。
每一个 catch 子句(异常处理程序)看起来就像一个接收且仅接收异常这种特殊类型参数的方法。当异常被抛出时,异常处理机制会负责搜寻参数与异常类型相匹配的第一个处理程序,然后进入子句中执行,此时便认为异常得到了处理。一旦 catch 子句结束,则处理程序的查找过程结束。

异常处理理论上有两种基本类型。

  • 终止模型,这种模型中,会假设错误的出现会让程序无法返回到异常发生的地方继续执行。异常被抛出意味着错误已经无法挽回。这也是 Java 和 C++ 所支持的模型。
  • 恢复模型,异常处理程序的工作室修正错误,然后尝试重新调用出问题的方法。在 Java 中可以在 while 循环内放入 try 块,达到类似的效果。这种效果一般会导致耦合度过高--恢复性处理程序的出口一般是非通用型代码(针对特殊异常情况),不好维护。

12.4 创建自定义异常

自己定义异常类,必须从已有的异常类继承。
建立新的异常类型后,编译器创建默认构造器,他将自动调用基类的默认构造器。
也可以定义一个接受字符串参数的构造器作为错误信息输出。

对于调用了在 Throwable 类声明的 printStackTrace() 的方法,将打印“从方法调用处直到异常抛出处”的方法调用序列,信息被发送到 System.out,并自动地被捕获和显示在输出中。
如果调用默认版本 e.printStackTrace(),则信息将被输出到标准错误流。

Logger 写入的最简单方式就是直接调用与日志记录消息的级别相关联的方法(例如 severe() 等)。
自定义异常中可以添加自定义域以满足需求。


12.5 异常说明

异常说明是属于方法声明的一部分,紧跟在形式参数列表之后。Java 中强制使用这种语法,以告知使用该方法的人可能抛出的异常类型。使用关键字 throws,后面接一个所有潜在异常类型的列表。
但是,对于从 RuntimeException 继承的异常,可以在没有异常说明的情况下被抛出。

声明方法的时候可以抛出异常,实际上却不抛出。编译器会相信这个声明,并强制此方法的用户像真的抛出异常那样使用这个方法。


12.6 捕获所有异常

catch(rException e) {
    // handle exception
}

使用上面的方式可以捕获所有的异常。

调用从基类 Throwable 继承的方法:

  • String getMessage() 用来获取异常的详细信息;
  • 使用 String getLocalizedMessage() 获取本地语言表示的详细信息
  • 使用 toString() 获取对 Throwable 的简单描述,要是有详细信息

打印 ThrowableThrowable 的调用栈轨迹,调用栈显示了“把你带到异常抛出地点”的方法调用序列:

  • void printStackTrace() 输出到标准错误
  • void printStackTrace(PrintStream) 输出到流中
  • void printStackTrace(java.io.PrintWriter) 输出到流中

Throwable fillInStackTrace() 用于在 Throwable 对象的内部记录栈帧的当前状态。
getClass() 也可以用来获取异常对象的名称等属性。

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


可以使用 throw e 将在 catch 中捕获到的异常重新跑出去。
当只是把当前异常对象重新抛出时,printStackTrace() 方法显示的将是原来异常抛出点的调用栈信息,而不是重新抛出点的信息。
想要更新这个信息,可以调用 fillInStackTrace() 方法,这将会返回一个 Throwable 对象,这个对象是通过把当前调用栈信息填入原来那个异常对象而建立的(即调用 fillInStackTrace() 方法处就是异常的新发生地)。


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

Throwable 的子类中,只有三种基本的异常类提供了带 cause 参数的构造器--ErrorExceptionRuntimeException。如果要把其他类型的异常链接起来,应该使用 initCause() 方法而不是构造器。
(个别细节不清楚,再看一遍)


12.7 Java 标准异常

Throwable 被用来表示任何可以被作为异常抛出的类。

Throwable 对象可以分为两种类型(指从 Throwable 继承而得到的类型):

  • Error 用来表示编译时和系统错误(除特殊情况外,一般不用关心)
  • Exception 是可以被抛出的基本类型。

异常的基本概念是用名称代表发生的问题(望文知意)。


特例:RuntimeException 异常
对于 RuntimeException 以及其他继承他的异常类,他们不需要在异常说明中声明方法将抛出 RuntimeException 类型异常(或任何从 RuntimeException 继承的异常,因此也被称为“不受检查的异常”。这种异常属于错误,将被系统自动捕获,不需要亲自处理-->其代表的是编程错误:

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

对于 RuntimeException 这种异常类型,编译器不需要异常说明,其输出将被直接报告给 System.error


12.8 使用 finally 进行清理

如果希望一段代码,无论 try 块中的异常是否被抛出,都能得到执行,name 可以使用 finally 子句。
用到 finally 的几种情况:

  1. 把内存之外的资源恢复到他们的初始状态,包括:已经打开的文件、网络连接、外部开关、图形等
  2. 在异常没有被当前的异常处理程序捕获的情况下,需要执行一些代码
  3. 涉及 break 和 continue 语句时,也需要执行的代码
  4. 即使在 try 块中 return 之后,也需要执行的代码

在 try 块中使用 try...finally... 结构会导致意外的异常丢失,这是 Java 异常实现的缺憾(针对 Java SE6之前版本)


12.9 异常的限制

在覆盖方法的时候,只能抛出在基类方法的异常说明里列出的那些异常。-->这个限制意味着,当基类使用的代码应用到其派生类对象的时候,一样能够工作(包括异常也能工作)。

针对构造器以及继承或实现的方法有几点:

  • 异常限制对构造器不起作用,构造器可以抛出任何异常,而不必理会基类构造器所抛出的异常。
  • 然而基类构造器必须以这样或那样的方式被调用(这里默认构造器将自动被调用)后,派生类构造器的异常说明就必须包含基类构造器的异常说明。
  • 派生类构造器不能捕获基类构造器抛出的异常(意味着只能抛出)。通过强制派生类遵守基类方法的异常说明,对象的可替换性得到了保证。
  • 派生类的方法可以选择不抛出任何异常,即使它是基类所定义的异常。
  • 使用派生类时,编译器只会强制要求捕获该派生类所抛出的异常;但是如果将其向上转型,那么编译器就会要求捕获基类抛出的异常。
  • 异常说明本身不属于方法类型的一部分,方法类型是由方法的名字与参数的类型组成的。因此不能基于异常说明来重载方法。
  • 一个出现在基类方法的异常说明中的异常,不一定会出现在炮声类方法的异常说明里。与继承中,基类的方法必须出现在派生类里的这种方法相比较,在继承和覆盖的过程中,某个特定方法的“异常说明的接口”是变小了的,与类方法的继承正好相反

12.10 构造器

对于构造器被调用时产生的异常,如果简单的使用 try...catch...finally 结构来处理异常,容易丢失掉异常,并且不能完成finally 内的代码逻辑,或是在不希望的情况下去完成了 finally 下的逻辑。对于这种情况,需要再用一层 try...catch 来捕获这个容易丢失的异常。
如下:

public static void main(String[] args) {
    try {
        InputFile in = new InputFile("Cleanup.java");
        try {
            String s;
            int i = 1;
            while ((s = in.getLine()) != null) {
                // Perform line-by-line processing here...
            }
        } catch (Exception e) {
            System.out.println("Caught Exception in main");
            e.printStackTrace(System.out);
        } finally {
            in.dispose();
        }
    } catch (Exception e) {
        System.out.println("InputFile construction failed");
    }
}

在构造之后以及创建一个新的 try 块,将构造与其他可能抛出异常的逻辑区分开,这样不会让 finally 内的逻辑被意外执行。
这种方式的基本规则是,在创建需要清理的对象之后,立即进入一个 try...finally 语句块。

总之在创建构造器的时候,如果容易产生异常,应该仔细考虑如何处理构造器的异常。


12.11 异常匹配

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

查找的时候并不要求抛出的异常和处理程序所声明的异常完全匹配。派生类的对象也可以匹配其基类的处理程序。
catch(xxxException e) 会捕获 xxxException 以及所有从他派生的异常。也就是说,如果捕获基类异常,那么在方法内加上更多派生的异常的时候,就无需更改程序。

如果把捕获基类的 catch 子句放在最前面,然后将后面放上派生类异常的 catch 子句,那么编译器会发现无法捕获派生类的异常,然后就会报错。


12.12 其他可选方式

异常,代表了当前方法不能继续执行的情形。异常处理系统就像一个活门(trap door),使人可以放弃程序的正常执行序列。当异常情形发生的时候,正常的执行已经不重要了,这个时候就要用到这个“活门”。
异常处理的重要原则:只有在你知道如何处理的情况下才捕获异常。
异常处理的一个重要目标就是把错误处理的代码通错误发生的地点相分离。

“吞食则有害”(harmful if swallowed):这个概念,是由在没准备好处理错误的情况下,添加了 catch 子句,这样异常虽然可能已经发生了,但是编译器并未显示错误,异常好像消失了。

后面介绍了一些异常的发展史。。。看的想去念研究生。。。


12.13 异常使用指南

总结起来,应该在以下情况使用异常:

  1. 在恰当的级别处理问题。(在知道该如何处理的情况下才捕获异常)
  2. 解决问题并且重新调用产生异常的方法。
  3. 进行少许修补,然后绕过异常发生的地方继续执行。
  4. 用别的数据进行计算,以代替方法预计会返回的值。
  5. 把当前运行环境下能做的事情尽量做完,然后把相同的异常抛到更高层。
  6. 把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层。
  7. 终止程序。
  8. 进行简化。(如果异常模式使问题变得太复杂,那么会很难使用)
  9. 让类库和程序更安全。(这既是在为调试做短期投资,也是为程序的健壮性做长期投资)
posted @ 2018-10-15 16:49  chentnt  阅读(382)  评论(0编辑  收藏  举报