Java 的基本理念是“结构不佳的代码不能运行”。
改进的错误恢复机制是提高代码健壮性的最强有力的方式。错误恢复在我们所编写的每一个程序中都是基本的要素,但是在 Java 中它显得格外重要,因为 Java 的主要目标之一就是创建供他人使用的程序构件。
发现错误的理想时机是在编译期。然而,编译期并不能找出所有错误,余下问题必须在运行时解决。这就需要错误源能通过某种方式,把适当的信息传递给知道如何正确处理这个问题的接收者。
要想创建健壮的系统,它的每一个构件都必须是健壮的。
Java 使用异常来提供一致的错误报告模型,使得构件能够与客户端代码可靠地沟通问题。
- Java 中的异常处理的目的
通过使用少于目前数量的代码来简化大型、可靠的程序的生成,并且通过这种方式可以使你更加确信你的应用中没有未处理的错误。异常的相关知识学起来并非艰涩难懂,并且它属于那种可以使你的项目受益明显、立竿见影的特性。
因为异常处理是 Java 中唯一官方的错误报告机制,并且通过编译器强制执行,所以不学习异常处理的话,也就只能写出那么些例子了。本章重在如何编写正确的异常处理程序,当方法出问题的时候,如何产生自定义的异常。
1 异常概念
1.1 历史中所谓的“异常”
C 以及其他早期语言常常具有多种错误处理模式,这些模式往往建立在约定俗成的基础上,并不属于语言规范。
通常是会返回某个特殊值或者设置某个标志,并且假定接收者将对这个返回值或标志进行检查,以判定是否发生了错误。
然而,随着时间的推移,人们发现,高傲的程序员们在使用程序库的时候更倾向于认为:“对,错误也许会发生,但那是别人造成的,不关我的事”。所以,程序员不去检查错误情形也就不足为奇了(对某些错误情形的检查确实无聊)。如果的确在每次调用方法的时候都彻底地进行错误检查,代码很可能会变得难以阅读。
正是由于程序员还仍然用这些方式拼凑系统,所以他们拒绝承认这样一个事实:对于构造大型、健壮、可维护的程序而言,这种错误处理模式已经成为了主要障碍。
解决方案
强制规定形式,来消除错误处理过程中的随心所欲。
这种做法由来已久,对异常处理的实现可以追溯到1960s的操作系统,甚至BASIC中的“on error goto”语句。C++的异常处理机制基于 Ada,Java 中的异常处理机制则建立在 C++ 的基础上。
“异常”这个词有“我对此感到意外”的意思。问题出现你也许不清楚如何处理,但你知道不该置之不理,你要停下来看看是不是有地方能够处理这个问题。
只是在当前的环境中还没有足够的信息来解决这个问题,所以就把这个问题提交到一个更高级别的环境中,在那里将作出正确的决定。
异常往往能降低错误处理代码的复杂度。
如果不使用异常,那么就必须检查特定的错误,并在程序中的许多地方去处理它。而如果使用异常,那就不必在方法调用处检查,因为异常机制将保证能够捕获这个错误。理想情况下,只需在一个地方处理错误,即所谓的异常处理程序。
这种方式不仅节省代码,而且把“描述在正常执行过程中做什么事”的代码和“出了问题怎么办”的代码相分离。总之,与以前的错误处理方法相比,异常机制使代码的阅读、编写和调试工作更加井井有条。
2 基本异常
注意把异常情形与普通问题相区分:
- 普通问题
在当前环境下能得到足够的信息,总能处理这个错误 - 异常情形(exceptional condition)
阻止当前方法或作用域继续执行,就不能继续下去了,因为在当前环境下无法获得必要的信息来解决问题。你所能做的就是从当前环境跳出,并且把问题提交给上一级环境。这就是抛出异常时所发生的事情。
简单的例子
除法就是一个除数有可能为 0,所以先进行检查很有必要。
但除数为 0 代表的究竟是什么意思呢?通过当前正在解决的问题环境,或许能知道该如何处理除数为 0 的情况。但如果这是一个意料之外的值,你也不清楚该如何处理,那就要抛出异常,而不是顺着原来的路径继续执行下去。
抛出异常后
- 同 Java 中其他对象的创建一样,将使用 new 在堆上创建异常对象
- 当前的执行路径(无法继续)被终止,并且从当前环境中弹出对异常对象的引用
- 异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。这个恰当的地方就是异常处理程序,它的任务是将程序从错误状态中恢复,以使程序能要么换一种方式运行,要么继续运行下去。
抛出异常的简单例子
对于对象引用 t,传给你可能尚未初始化。所以在使用这个对象引用调用其方法之前,会先对引用进行检查。可以创建一个代表错误信息的对象,并且将它从当前环境中“抛出”,这样就把错误信息传播到了“更大”的环境中。
这被称为抛出一个异常,看起来像这样:
这就抛出了异常,于是在当前环境下就不必再为这个问题操心了,它将在别的地方得到处理。
异常模型的观点
- 异常使得我们可以将每件事都当作一个事务来考虑,而异常可以看护着这些事务的底线,事务的基本保障是我们所需的在分布式计算中的异常处理。事务是计算机中的合同法,如果出了什么问题,我们只需要放弃整个计算
- 我们还可以将异常看作是一种内建的撤销系统,因为(在细心使用的情况下)我们在程序中可以拥有各种不同的撤销点。如果程序的某部分失败了,异常将撤销到程序中某个已知的稳定点上。
异常最重要的方面之一就是如果发生问题,它们将不允许程序沿着其正常的路径继续走下去。在 C 和 C++ 这样的语言中,这可真是个问题,尤其是 C,它没有任何办法可以强制程序在出现问题时停止在某条路径上运行下去,因此我们有可能会较长时间地忽略问题,从而会陷入完全不恰当的状态中。异常允许我们(如果没有其他手段)强制程序停止运行,并告诉我们出现了什么问题,或者(理想状态下)强制程序处理问题,并返回到稳定状态。
异常参数
与使用 Java 中的其他对象一样,用 new 在堆上创建异常对象,这也伴随着存储空间的分配和构造器的调用。
所有标准异常类都有两个构造器:
- 无参构造器
- 接受字符串作为参数,以便能把相关信息放入异常对象构造器
- 关键字 throw 将产生许多有趣的结果。在使用 new 创建了异常对象之后,此对象的引用将传给 throw。
尽管异常对象的类型通常与方法设计的返回类型不同,但从效果上看,它就像是从方法“返回”的。可以简单地把异常处理看成一种不同的返回机制,当然若过分强调这种类比的话,就会有麻烦了。
另外还能用抛出异常的方式从当前的作用域退出。在这两种情况下,将会返回一个异常对象,然后退出方法或作用域。
抛出异常与方法正常返回的相似之处到此为止。因为异常返回的“地点”与普通方法调用返回的“地点”完全不同。
异常将在一个恰当的异常处理程序中得到解决,它的位置可能离异常被抛出的地方很远,也可能会跨越方法调用栈的许多层级。
此外,能够抛出任意类型的 Throwable 对象,它是异常类型的根类。通常,对于不同类型错误,要抛相应异常。错误信息可以保存在异常对象内部或者用异常类的名称来暗示。上一层环境通过这些信息来决定如何处理异常。
通常,唯一的信息只有异常的类型名,而在异常对象内部没有任何有意义的信息
3 异常捕获
- 首先要理解监控区域(guarded region)的概念
它是一段可能产生异常的代码,并且后面跟着处理这些异常的代码。
3.1 try 块
如果在方法内部抛异常(或在方法内部调用的其他方法抛异常),该方法将在抛异常过程中结束。要是不希望方法就此结束,可在方法内设置一个特殊的块来捕获异常。因为在这个块里“尝试”各种(可能产生异常的)方法调用,所以称为 try 块。它是跟在 try 关键字之后的普通程序块:
意义
对于不支持异常处理的程序语言,要想仔细检查错误,就得在每个方法调用的前后加上设置和错误检查的代码,甚至在每次调用同一方法时也得这么做。
有了异常处理机制,可以把所有动作都放在 try 块,然后只需在一个地方就可以捕获所有异常。这意味着你的代码将更容易编写和阅读,因为代码的意图和错误检查不是混淆在一起。
3.2 异常处理程序 - catch 块
抛出的异常必须在 异常处理程序 得到处理。针对每个要捕获的异常,得准备相应的处理程序。
异常处理程序紧跟在 try 块之后,以关键字 catch 表示:
每个 catch (异常处理程序)看起来就像是接收且仅接收一个特殊类型的参数的方法。可以在处理程序的内部使用标识符(id1,id2 等等),这与方法参数的使用很相似。有时可能用不到标识符,因为异常的类型已经给了你足够的信息来对异常进行处理,但标识符并不可以省略。
异常处理程序必须紧跟在 try 块后。当异常被抛出时,异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序。然后进入 catch 子句执行,此时认为异常得到了处理。
一旦 catch 子句结束,则处理程序的查找过程结束。注意,只有匹配的 catch 子句才能得到执行。
意义
注意在 try 块的内部,许多不同的方法调用可能会产生类型相同的异常,而你只需要提供一个针对此类型的异常处理程序。
3.3 终止与恢复
异常处理理论上有两种基本模型。
3.3.1 终止模型
Java 和 C++所支持的模型。
将假设错误非常严重,以至于程序无法返回到异常发生的地方继续执行。一旦异常被抛出,就表明错误无法挽回。
3.3.2 恢复模型
异常处理程序的工作是修正错误,然后重新尝试调用出问题的方法,并认为第二次能成功。对于恢复模型,通常希望异常被处理之后能继续执行程序。
如果想要用 Java 实现类似恢复的行为
- 那么在遇见错误时就不能抛出异常,而是调用方法来修正该错误
- 或者把 try 块放在 while 循环里,这样就不断地进入 try 块,直到得到满意的结果
缺陷
在过去,使用支持恢复模型异常处理的操作系统的程序员们最终还是转向使用类似“终止模型”的代码,并且忽略恢复行为。所以虽然恢复模型开始显得很吸引人,但不是很实用。其中的主要原因可能是它所导致的耦合:恢复性的处理程序需要了解异常抛出的地点,这势必要包含依赖于抛出位置的非通用性代码。这增加了代码编写和维护的困难,对于异常可能会从许多地方抛出的大型程序来说,更是如此。
4 自定义异常
Java 提供的异常体系不可能预见所有的希望加以报告的错误,所以可自己定义异常类。
必须从已有的异常类继承,最好是选择意思相近的异常类继承(不过并不容易找)。建立新的异常类型最简单的方法就是让编译器为你产生
无参构造器
输出为:
编译器创建了无参构造器,它将自动调用基类的无参构造器。对异常来说,最重要的部分就是类名。
字符串参数的构造器
输出为:
新增的代码非常简短:两个构造器定义了 MyException 类型对象的创建方式。对于第二个构造器,使用 super 关键字明确调用了其基类构造器,它接受一个字符串作为参数。
在异常处理程序中,调用了在 Throwable 类声明(Exception 即从此类继承)的 printStackTrace() 方法。就像从输出中看到的,它将打印“从方法调用处直到异常抛出处”的方法调用序列。这里,信息被发送到了 System.out,并自动地被捕获和显示在输出中。但是,如果调用默认版本:
信息就会被输出到标准错误流。
4.1 记录日志
- 使用 java.util.logging 工具将输出记录到日志
- 输出
- 直接调用与日志记录消息的级别相关联的方法,这里是 severe()。
为了产生日志记录消息,要获取异常抛出处的栈轨迹,但是 printStackTrace() 不会默认地产生字符串。为了获取字符串,我们需要使用重载的 printStackTrace() 方法,它接受一个 java.io.PrintWriter 对象作为参数(PrintWriter 会在。如果我们将一个 java.io.StringWriter 对象传递给这个 PrintWriter 的构造器,那么通过调用 toString() 方法,就可以将输出抽取为一个 String。
尽管由于 LoggingException 将所有记录日志的基础设施都构建在异常自身中,使得它所使用的方式非常方便,并因此不需要客户端程序员的干预就可以自动运行。
捕获和记录其他人编写的异常
必须在异常处理程序中生成日志消息
结果
加入额外构造器和成员
输出为:
新的异常添加了字段 x 以及设定 x 值的构造器和读取数据的方法。此外,还覆盖了 Throwable.
getMessage() 方法,以产生更详细的信息。对于异常类来说,getMessage() 方法有点类似于 toString() 方法。
既然异常也是对象的一种,所以可以继续修改这个异常类,以得到更强的功能。但要记住,使用程序包的客户端程序员可能仅仅只是查看一下抛出的异常类型,其他的就不管了(大多数 Java 库里的异常都是这么用的),所以对异常所添加的其他功能也许根本用不上。
5 异常声明
Java 鼓励把方法可能会抛出的异常告知使用此方法的客户端程序员。这是种优雅的做法,它使得调用者能确切知道写什么样的代码可以捕获所有潜在的异常。
当然,如果提供了源代码,客户端程序员可以在源代码中查找 throw 语句来获知相关信息,然而程序库通常并不与源代码一起发布。为了预防这样的问题,Java 提供了相应的语法(并强制使用这个语法),使你能以礼貌的方式告知客户端程序员某个方法可能会抛出的异常类型,然后客户端程序员就可以进行相应的处理。这就是异常说明,它属于方法声明的一部分,紧跟在形式参数列表之后。
异常说明使用了附加的关键字 throws,后面接一个所有潜在异常类型的列表,所以方法定义可能看起来像这样:
但要是这样写:
就表示此方法不会抛任何异常。
代码必须与异常说明保持一致。如果方法里的代码产生了异常却没有进行处理,编译器会发现这个问题并提醒你:
- 要么处理这个异常
- 要么就在异常说明中表明此方法将产生异常
通过这种自顶向下强制执行的异常说明机制,Java 在编译时就可以保证一定的异常正确性。
“作弊”的地方
可以声明方法将抛异常,实际上却不抛出。编译器相信了这个声明,并强制此方法的用户像真的抛出异常那样使用这个方法。这样做的好处是,为异常先占个位子,以后就可以抛出这种异常而不用修改已有的代码。在定义抽象基类和接口时这种能力很重要,这样派生类或接口实现就能够抛出这些预先声明的异常。
这种在编译时被强制检查的异常称为被检查的异常。















