mirror of
https://github.com/LingCoder/OnJava8.git
synced 2026-08-24 06:53:27 +08:00
revision[15] end in rethrowing exception
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
> Java 的基本理念是“结构不佳的代码不能运行”。
|
||||
|
||||
改进的错误恢复机制是提供代码健壮性的最强有力的方式。错误恢复在我们所编写的每一个程序中都是基本的要素,但是在 Java 中它显得格外重要,因为 Java 的主要目标之一就是创建供他人使用的程序构件。
|
||||
改进的错误恢复机制是提高代码健壮性的最强有力的方式。错误恢复在我们所编写的每一个程序中都是基本的要素,但是在 Java 中它显得格外重要,因为 Java 的主要目标之一就是创建供他人使用的程序构件。
|
||||
|
||||
发现错误的理想时机是在编译阶段,也就是在你试图运行程序之前。然而,编译期间并不能找出所有的错误,余下的问题必须在运行期间解决。这就需要错误源能通过某种方式,把适当的信息传递给某个接收者——该接收者将知道如何正确处理这个问题。
|
||||
|
||||
@@ -21,27 +21,25 @@ Java 中的异常处理的目的在于通过使用少于目前数量的代码来
|
||||
|
||||
## 异常概念
|
||||
|
||||
<!-- Basic Exceptions -->
|
||||
|
||||
C 以及其他早期语言常常具有多种错误处理模式,这些模式往往建立在约定俗成的基础之上,而并不属于语言的一部分。通常会返回某个特殊值或者设置某个标志,并且假定接收者将对这个返回值或标志进行检查,以判定是否发生了错误。然而,随着时间的推移,人们发现,高傲的程序员们在使用程序库的时候更倾向于认为:“对,错误也许会发生,但那是别人造成的,不关我的事”。所以,程序员不去检查错误情形也就不足为奇了(何况对某些错误情形的检查确实很无聊)。如果的确在每次调用方法的时候都彻底地进行错误检查,代码很可能会变得难以阅读。正是由于程序员还仍然用这些方式拼凑系统,所以他们拒绝承认这样一个事实:对于构造大型、健壮、可维护的程序而言,这种错误处理模式已经成为了主要障碍。
|
||||
|
||||
解决的办法是,用强制规定的形式来消除错误处理过程中随心所欲的因素。这种做法由来已久,对异常处理的实现可以迫溯到 20 世纪 60 年代的操作系统,甚至于 BASIC 语言中的“on error goto”语句。而 C++的异常处理机制基于 Ada,Java 中的异常处理则建立在 C++的基础之上(尽管看上去更像 Object Pascal)。
|
||||
解决的办法是,用强制规定的形式来消除错误处理过程中随心所欲的因素。这种做法由来已久,对异常处理的实现可以追溯到 20 世纪 60 年代的操作系统,甚至于 BASIC 语言中的“on error goto”语句。而 C++的异常处理机制基于 Ada,Java 中的异常处理机制则建立在 C++的基础之上(尽管看上去更像 Object Pascal)。
|
||||
|
||||
“异常”这个词有“我对此感到意外”的意思。问题出现了,你也许不清楚该如何处理,但你的确知道不应该置之不理,你要停下来,看看是不是有别人或在别的地方,能够处理这个问题。只是在当前的环境中还没有足够的信息来解决这个问题,所以就把这个问题提交到一个更高级别的环境中,在这里将作出正确的决定。
|
||||
“异常”这个词有“我对此感到意外”的意思。问题出现了,你也许不清楚该如何处理,但你的确知道不应该置之不理,你要停下来,看看是不是有别人或在别的地方,能够处理这个问题。只是在当前的环境中还没有足够的信息来解决这个问题,所以就把这个问题提交到一个更高级别的环境中,在那里将作出正确的决定。
|
||||
|
||||
异常往往能降低错误处理代码的复杂度。如果不使用异常,那么就必须检查特定的错误,并在程序中的许多地方去处理它。而如果使用异常,那就不必在方法调用处进行检查,因为异常机制将保证能够捕获这个错误。并且,只需在一个地方处理错误,即所谓的异常处理程序中。这种方式不仅节省代码,而且把“描述在正常执行过程中做什么事”的代码和“出了问题怎么办”的代码相分离。总之,与以前的错误处理方法相比,异常机制使代码的阅读、编写和调试工作更加井井有条。
|
||||
异常往往能降低错误处理代码的复杂度。如果不使用异常,那么就必须检查特定的错误,并在程序中的许多地方去处理它。而如果使用异常,那就不必在方法调用处进行检查,因为异常机制将保证能够捕获这个错误。理想情况下,只需在一个地方处理错误,即所谓的异常处理程序中。这种方式不仅节省代码,而且把“描述在正常执行过程中做什么事”的代码和“出了问题怎么办”的代码相分离。总之,与以前的错误处理方法相比,异常机制使代码的阅读、编写和调试工作更加井井有条。
|
||||
|
||||
<!-- Basic Exceptions -->
|
||||
|
||||
## 基本异常
|
||||
|
||||
<!-- Catching an Exception -->
|
||||
|
||||
异常情形(exceptional condition)是指阻止当前方法或作用域继续执行的问题。把异常情形与普通问题相区分很重要,所谓的普通问题是指,在当前环境下能得到足够的信息,总能处理这个错误。而对于异常情形,就不能继续下去了,因为在当前环境下无法获得必要的信息来解决问题。你所能做的就是从当前环境跳出,并且把问题提交给上一级环境。这就是抛出异常时所发生的事情。
|
||||
|
||||
除法就是一个简单的例子。除数有可能为 0,所以先进行检查很有必要。但除数为 0 代表的究竟是什么意思呢?通过当前正在解决的问题环境,或许能知道该如何处理除数为 0 的情况。但如果这是一个意料之外的值,你也不清楚该如何处理,那就要抛出异常,而不是顺着原来的路径继续执行下去。
|
||||
|
||||
当抛出异常后,有几件事会随之发生。首先,同 Java 中其他对象的创建一样,将使用 new 在堆上创建异常对象。然后,当前的执行路径(它不能继续下去了)被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。这个恰当的地方就是异常处理程序,它的任务是将程序从错误状态中恢复,以使程序能要么换一种方式运行,要么继续运行下去。
|
||||
|
||||
举一个抛出异常的简单例子。对于对象引用 t,传给你的时候可能尚未被初始化。所以在使用这个对象引用调用其方法之前,会先对引用进行检查。可以创建一个代表错误信息的对象,并且将它从当前环境中“抛出”,这样就把错误信息传播到了“更大”的环境中。这被称为抛出一个异常,看起来像这样:
|
||||
举一个抛出异常的简单例子。对于对象引用 t,传给你的时候可能尚未被初始化。所以在使用这个对象引用调用其方法之前,会先对引用进行检查。可以创建一个代表错误信息的对象,并且将它从当前环境中“抛出”,这样就把错误信息传播到了“更大”的环境中。这被称为*抛出一个异常*,看起来像这样:
|
||||
|
||||
```java
|
||||
if(t == null)
|
||||
@@ -52,7 +50,9 @@ if(t == null)
|
||||
|
||||
异常使得我们可以将每件事都当作一个事务来考虑,而异常可以看护着这些事务的底线“…事务的基本保障是我们所需的在分布式计算中的异常处理。事务是计算机中的合同法,如果出了什么问题,我们只需要放弃整个计算。”我们还可以将异常看作是一种内建的恢复(undo)系统,因为(在细心使用的情况下)我们在程序中可以拥有各种不同的恢复点。如果程序的某部分失败了,异常将“恢复”到程序中某个已知的稳定点上。
|
||||
|
||||
异常最重要的方面之一就是如果发生问题,它们将不允许程序沿着其正常的路径继续走下去。在 C 和 C++这样的语言中,这可真是个问题,尤其是 C,它没有任何办法可以强制程序在出现问题时停止在某条路径上运行下去,因此我们有可能会较长时间地忽略了问题,从而陷入了完全不恰当的状态中。异常允许我们(如果没有其他手段)强制程序停止运行,并告诉我们出现了什么问题,或者(理想状态下)强制程序处理问题,并返回到稳定状态。
|
||||
异常最重要的方面之一就是如果发生问题,它们将不允许程序沿着其正常的路径继续走下去。在 C 和 C++ 这样的语言中,这可真是个问题,尤其是 C,它没有任何办法可以强制程序在出现问题时停止在某条路径上运行下去,因此我们有可能会较长时间地忽略问题,从而会陷入完全不恰当的状态中。异常允许我们(如果没有其他手段)强制程序停止运行,并告诉我们出现了什么问题,或者(理想状态下)强制程序处理问题,并返回到稳定状态。
|
||||
|
||||
<!-- Exception Arguments -->
|
||||
|
||||
### 异常参数
|
||||
|
||||
@@ -64,9 +64,11 @@ throw new NullPointerException("t = null");
|
||||
|
||||
不久读者将看到,要把这个字符串的内容提取出来可以有多种不同的方法。
|
||||
|
||||
关键字 throw 将产生许多有趣的结果。在使用 new 创建了异常对象之后,此对象的引用将传给 throw。尽管返回的异常对象其类型通常与方法设计的返回类型不同,但从效果上看,它就像是从方法“返回”的。可以简单地把异常处理看成一种不同的返回机制,当然若过分强调这种类比的话,就会有麻烦了。另外还能用抛出异常的方式从当前的作用域退出。在这两种情况下,将会返回一个异常对象,然后退出方法或作用域。
|
||||
关键字 **throw** 将产生许多有趣的结果。在使用 **new** 创建了异常对象之后,此对象的引用将传给 **throw**。尽管异常对象的类型通常与方法设计的返回类型不同,但从效果上看,它就像是从方法“返回”的。可以简单地把异常处理看成一种不同的返回机制,当然若过分强调这种类比的话,就会有麻烦了。另外还能用抛出异常的方式从当前的作用域退出。在这两种情况下,将会返回一个异常对象,然后退出方法或作用域。
|
||||
|
||||
抛出异常与方法正常返回值的相似之处到此为止。因为异常返回的“地点”与普通方法调用返回的“地点”完全不同。(异常将在一个恰当的异常处理程序中得到解决,它的位置可能离异常被抛出的地方很远,也可能会跨越方法调用栈的许多层次。)此外,能够抛出任意类型的 Throwable 对象,它是异常类型的根类。通常,对于不同类型的错误,要抛出相应的异常。错误信息可以保存在异常对象内部或者用异常类的名称来暗示。上一层环境通过这些信息来决定如何处理异常。(通常,异常对象中仅有的信息就是异常类型,除此之外不包含任何有意义的内容。)
|
||||
抛出异常与方法正常返回的相似之处到此为止。因为异常返回的“地点”与普通方法调用返回的“地点”完全不同。(异常将在一个恰当的异常处理程序中得到解决,它的位置可能离异常被抛出的地方很远,也可能会跨越方法调用栈的许多层级。)
|
||||
|
||||
此外,能够抛出任意类型的 **Throwable** 对象,它是异常类型的根类。通常,对于不同类型的错误,要抛出相应的异常。错误信息可以保存在异常对象内部或者用异常类的名称来暗示。上一层环境通过这些信息来决定如何处理异常。(通常,唯一的信息只有异常的类型名,而在异常对象内部没有任何有意义的信息。)
|
||||
|
||||
## 异常捕获
|
||||
|
||||
@@ -82,7 +84,7 @@ try {
|
||||
}
|
||||
```
|
||||
|
||||
对于不支持异常处理的程序语言,要想仔细检查错误,就得在每个方法调用的前后加上设置和错误检查的代码,甚至在每次调用同一方法时也得这么做。有了异常处理机制,可以把所有动作都放在 try 块里,然后只需在一个地方就可以捕获所有异常。这意味着你的代码将更容易编写和阅读,因为代码的目标是弄清楚错误检查。
|
||||
对于不支持异常处理的程序语言,要想仔细检查错误,就得在每个方法调用的前后加上设置和错误检查的代码,甚至在每次调用同一方法时也得这么做。有了异常处理机制,可以把所有动作都放在 try 块里,然后只需在一个地方就可以捕获所有异常。这意味着你的代码将更容易编写和阅读,因为代码的意图和错误检查不是混淆在一起的。
|
||||
|
||||
### 异常处理程序
|
||||
|
||||
@@ -101,7 +103,7 @@ try {
|
||||
// etc.
|
||||
```
|
||||
|
||||
每个 catch 子句(异常处理程序)看起来就像是接收一个且仅接收一个特殊类型的参数的方法。可以在处理程序的内部使用标识符(id1,id2 等等),这与方法参数的使用很相似。有时可能用不到标识符,因为异常的类型已经给了你足够的信息来对异常进行处理,但标识符并不可以省略。
|
||||
每个 catch 子句(异常处理程序)看起来就像是接收且仅接收一个特殊类型的参数的方法。可以在处理程序的内部使用标识符(id1,id2 等等),这与方法参数的使用很相似。有时可能用不到标识符,因为异常的类型已经给了你足够的信息来对异常进行处理,但标识符并不可以省略。
|
||||
|
||||
异常处理程序必须紧跟在 try 块之后。当异常被抛出时,异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序。然后进入 catch 子句执行,此时认为异常得到了处理。一旦 catch 子句结束,则处理程序的查找过程结束。注意,只有匹配的 catch 子句才能得到执行;这与 switch 语句不同,switch 语句需要在每一个 case 后面跟一个 break,以避免执行后续的 case 子句。
|
||||
|
||||
@@ -109,11 +111,11 @@ try {
|
||||
|
||||
### 终止与恢复
|
||||
|
||||
异常处理理论上有两种基本模型。Java 支持终止模型(它是 Java 和 C++所支持的模型)。在这种模型中,将假设错误非常关键,以至于程序无法返回到异常发生的地方继续执行。一旦异常被抛出,就表明错误已无法挽回,也不能回来继续执行。
|
||||
异常处理理论上有两种基本模型。Java 支持终止模型(它是 Java 和 C++所支持的模型)。在这种模型中,将假设错误非常严重,以至于程序无法返回到异常发生的地方继续执行。一旦异常被抛出,就表明错误已无法挽回,也不能回来继续执行。
|
||||
|
||||
另一种称为恢复模型。意思是异常处理程序的工作是修正错误,然后重新尝试调用出问题的方法,并认为第二次能成功。对于恢复模型,通常希望异常被处理之后能继续执行程序。如果想要用 Java 实现类似恢复的行为,那么在遇见错误时就不能抛出异常,而是调用方法来修正该错误。或者,把 try 块放在 while 循环里,这样就不断地进入 try 块,直到得到满意的结果。
|
||||
|
||||
长久以来,尽管程序员们使用的操作系统支持恢复模型的异常处理,但他们最终还是转向使用类似“终止模型”的代码,并且忽略恢复行为。所以虽然恢复模型开始显得很吸引人,但不是很实用。其中的主要原因可能是它所导致的耦合:恢复性的处理程序需要了解异常抛出的地点,这势必要包含依赖于抛出位置的非通用性代码。这增加了代码编写和维护的困难,对于异常可能会从许多地方抛出的大型程序来说,更是如此。
|
||||
在过去,使用支持恢复模型异常处理的操作系统的程序员们最终还是转向使用类似“终止模型”的代码,并且忽略恢复行为。所以虽然恢复模型开始显得很吸引人,但不是很实用。其中的主要原因可能是它所导致的耦合:恢复性的处理程序需要了解异常抛出的地点,这势必要包含依赖于抛出位置的非通用性代码。这增加了代码编写和维护的困难,对于异常可能会从许多地方抛出的大型程序来说,更是如此。
|
||||
|
||||
<!-- Creating Your Own Exceptions -->
|
||||
|
||||
@@ -265,7 +267,7 @@ LoggingExceptions.main(LoggingExceptions.java:25)
|
||||
Caught LoggingException
|
||||
```
|
||||
|
||||
静态的 Logger.getLogger() 方法创建了一个 String 参数相关联的 Logger 对象(通常与错误相关的包名和类名),这个 Logger 对象会将其输出发送到 System.err。向 Logger 写人的最简单方式就是直接调用与日志记录消息的级别相关联的方法,这里使用的是 severe()。为了产生日志记录消息,我们欲获取异常抛出处的栈轨迹,但是 printStackTrace() 不会默认地产生字符串。为了获取字符串,我们需要使用重载的 printStackTrace() 方法,它接受一 java.io.PrintWriter 对象作为参数(PrintWriter 会在[附录:I/O 流 ]() 一章详细介绍)。如果我们将一个 java.io.StringWriter 对象传递给这个 PrintWriter 的构造器,那么通过调用 toString() 方法,就可以将输出抽取为一个 String。
|
||||
静态的 Logger.getLogger() 方法创建了一个 String 参数相关联的 Logger 对象(通常与错误相关的包名和类名),这个 Logger 对象会将其输出发送到 System.err。向 Logger 写入的最简单方式就是直接调用与日志记录消息的级别相关联的方法,这里使用的是 severe()。为了产生日志记录消息,我们欲获取异常抛出处的栈轨迹,但是 printStackTrace() 不会默认地产生字符串。为了获取字符串,我们需要使用重载的 printStackTrace() 方法,它接受一个 java.io.PrintWriter 对象作为参数(PrintWriter 会在[附录:I/O 流 ]() 一章详细介绍)。如果我们将一个 java.io.StringWriter 对象传递给这个 PrintWriter 的构造器,那么通过调用 toString() 方法,就可以将输出抽取为一个 String。
|
||||
|
||||
尽管由于 LoggingException 将所有记录日志的基础设施都构建在异常自身中,使得它所使用的方式非常方便,并因此不需要客户端程序员的干预就可以自动运行,但是更常见的情形是我们需要捕获和记录其他人编写的异常,因此我们必须在异常处理程序中生成日志消息;
|
||||
|
||||
@@ -448,7 +450,7 @@ Throwable fillInStackTrace()
|
||||
|
||||
用于在 Throwable 对象的内部记录栈帧的当前状态。这在程序重新抛出错误或异常(很快就会讲到)时很有用。
|
||||
|
||||
此外,也可以使用 Throwable 从其基类 Object(也是所有类的基类)继承的方法。对于异常来说,getClass)也许是个很好用的方法,它将返回一个表示此对象类型的对象。然后可以使用 getName)方法查询这个 Class 对象包含包信息的名称,或者使用只产生类名称的 getSimple Name0 方法。
|
||||
此外,也可以使用 Throwable 从其基类 Object(也是所有类的基类)继承的方法。对于异常来说,getClass)也许是个很好用的方法,它将返回一个表示此对象类型的对象。然后可以使用 getName)方法查询这个 Class 对象包含包信息的名称,或者使用只产生类名称的 getSimpleName() 方法。
|
||||
|
||||
下面的例子演示了如何使用 Exception 类型的方法:
|
||||
|
||||
@@ -522,7 +524,7 @@ public class SameHandler {
|
||||
}
|
||||
```
|
||||
|
||||
通过 Java 7 的多重捕获机制,你可以讲不同类型的异常使用“或”将它们组合起来,只在一个 catch 块中使用:
|
||||
通过 Java 7 的多重捕获机制,你可以使用“或”将不同类型的异常组合起来,只需要一行 catch 语句:
|
||||
|
||||
```java
|
||||
// exceptions/MultiCatch.java
|
||||
@@ -532,6 +534,7 @@ public class MultiCatch {
|
||||
void f() {
|
||||
try {
|
||||
x();
|
||||
|
||||
} catch(Except1 | Except2 | Except3 | Except4 e) {
|
||||
process();
|
||||
}
|
||||
@@ -539,7 +542,7 @@ public class MultiCatch {
|
||||
}
|
||||
```
|
||||
|
||||
或者以其他组合的方式:
|
||||
或者以其他的组合方式:
|
||||
|
||||
```java
|
||||
// exceptions/MultiCatch2.java
|
||||
@@ -559,7 +562,7 @@ public class MultiCatch2 {
|
||||
}
|
||||
```
|
||||
|
||||
这对书写更整洁的代码很有帮助
|
||||
这对书写更整洁的代码很有帮助。
|
||||
|
||||
### 栈轨迹
|
||||
|
||||
|
||||
Reference in New Issue
Block a user