mirror of
https://github.com/LingCoder/OnJava8.git
synced 2026-08-19 12:33:29 +08:00
第15章Exceptions翻译修改 (#538)
* Update 15-Exceptions.md * Update 15-Exceptions.md * Update 15-Exceptions.md * Update 15-Exceptions.md 直接把有翻译有争议的句子改回原翻译 * Update 15-Exceptions.md
This commit is contained in:
@@ -15,7 +15,7 @@ Java 使用异常来提供一致的错误报告模型,使得构件能够与客
|
||||
|
||||
Java 中的异常处理的目的在于通过使用少于目前数量的代码来简化大型、可靠的程序的生成,并且通过这种方式可以使你更加确信:你的应用中没有未处理的错误。异常的相关知识学起来并非艰涩难懂,并且它属于那种可以使你的项目受益明显、立竿见影的特性之一。
|
||||
|
||||
因为异常处理是 Java 中唯一官方的错误报告机制,并且通过编译器强制执行,所以不学习异常处理的话,书中也就只能写出那么些例子了。本章将向读者介绍如何编写正确的异常处理程序,并将展示当方法出问题的时候,如何产生自定义的异常。
|
||||
因为异常处理是 Java 中唯一官方的错误报告机制,并且通过编译器强制执行,所以不学习异常处理的话,你也就只能写出书中那么些例子了。本章将教你如何编写正确的异常处理程序,以及当方法出问题的时候,如何产生自定义的异常。
|
||||
|
||||
<!-- Concepts -->
|
||||
|
||||
@@ -48,9 +48,7 @@ if(t == null)
|
||||
|
||||
这就抛出了异常,于是在当前环境下就不必再为这个问题操心了,它将在别的地方得到处理。具体是哪个“地方”后面很快就会介绍。
|
||||
|
||||
异常使得我们可以将每件事都当作一个事务来考虑,而异常可以看护着这些事务的底线“…事务的基本保障是我们所需的在分布式计算中的异常处理。事务是计算机中的合同法,如果出了什么问题,我们只需要放弃整个计算。”我们还可以将异常看作是一种内建的恢复(undo)系统,因为(在细心使用的情况下)我们在程序中可以拥有各种不同的恢复点。如果程序的某部分失败了,异常将“恢复”到程序中某个已知的稳定点上。
|
||||
|
||||
异常最重要的方面之一就是如果发生问题,它们将不允许程序沿着其正常的路径继续走下去。在 C 和 C++ 这样的语言中,这可真是个问题,尤其是 C,它没有任何办法可以强制程序在出现问题时停止在某条路径上运行下去,因此我们有可能会较长时间地忽略问题,从而会陷入完全不恰当的状态中。异常允许我们(如果没有其他手段)强制程序停止运行,并告诉我们出现了什么问题,或者(理想状态下)强制程序处理问题,并返回到稳定状态。
|
||||
异常允许你将做的每件事都当作一个事务来考虑,而异常守护着这些事务:“...事务的基本保障是,我们需要的分布式计算的异常处理机制。事务相当于计算机中的合同法。如果任何事出现了错误,我们只需要丢弃整个计算。”你也可以将异常看作一种内建的“恢复”(undo)系统,因为(在细心使用时)你在程序中可以有各种恢复点。一旦程序的一个部分失败了,异常将“恢复”到一个已知的稳定点上。
|
||||
|
||||
<!-- Exception Arguments -->
|
||||
|
||||
@@ -121,7 +119,7 @@ try {
|
||||
|
||||
## 自定义异常
|
||||
|
||||
不必拘泥于 Java 中已有的异常类型。Java 提供的异常体系不可能预见所有的希望加以报告的错误,所以可以自己定义异常类来表示程序中可能会遇到的特定问题。
|
||||
不必拘泥于 Java 已有的异常类型。Java异常体系不可能预见你将报告的所有错误,所以你可以创建自己的异常类,来表示你的程序中可能遇到的问题。
|
||||
|
||||
要自己定义异常类,必须从已有的异常类继承,最好是选择意思相近的异常类继承(不过这样的异常并不容易找)。建立新的异常类型最简单的方法就是让编译器为你产生无参构造器,所以这几乎不用写多少代码:
|
||||
|
||||
@@ -157,7 +155,7 @@ Caught it!
|
||||
|
||||
编译器创建了无参构造器,它将自动调用基类的无参构造器。本例中不会得到像 SimpleException(String) 这样的构造器,这种构造器也不实用。你将看到,对异常来说,最重要的部分就是类名,所以本例中建立的异常类在大多数情况下已经够用了。
|
||||
|
||||
本例的结果被打印到了控制台上,本书的输出显示系统正是在控制台上自动地捕获和测试这些结果的。但是,你也许想通过写入 System.err 而将错误发送给标准错误流。通常这比把错误信息输出到 System.out 要好,因为 System.out 也许会被重定向。如果把结果送到 System.err,它就不会随 System.out 一起被重定向,这样更容易被用户注意。
|
||||
本例的结果被显示在控制台。你也可以通过写入 System.err 而将错误发送给标准错误流。通常这比把错误信息输出到 System.out 要好,因为 System.out 也许会被重定向。如果把结果送到 System.err,它就不会随 System.out 一起被重定向,所以用户就更容易注意到它。
|
||||
|
||||
你也可以为异常类创建一个接受字符串参数的构造器:
|
||||
|
||||
@@ -197,13 +195,11 @@ public class FullConstructors {
|
||||
Throwing MyException from f()
|
||||
MyException
|
||||
at FullConstructors.f(FullConstructors.java:11)
|
||||
at
|
||||
FullConstructors.main(FullConstructors.java:19)
|
||||
at FullConstructors.main(FullConstructors.java:19)
|
||||
Throwing MyException from g()
|
||||
MyException: Originated in g()
|
||||
at FullConstructors.g(FullConstructors.java:15)
|
||||
at
|
||||
FullConstructors.main(FullConstructors.java:24)
|
||||
at FullConstructors.main(FullConstructors.java:24)
|
||||
```
|
||||
|
||||
新增的代码非常简短:两个构造器定义了 MyException 类型对象的创建方式。对于第二个构造器,使用 super 关键字明确调用了其基类构造器,它接受一个字符串作为参数。
|
||||
@@ -484,8 +480,7 @@ getLocalizedMessage():My Exception
|
||||
toString():java.lang.Exception: My Exception
|
||||
printStackTrace():
|
||||
java.lang.Exception: My Exception
|
||||
at
|
||||
ExceptionMethods.main(ExceptionMethods.java:7)
|
||||
at ExceptionMethods.main(ExceptionMethods.java:7)
|
||||
```
|
||||
|
||||
可以发现每个方法都比前一个提供了更多的信息一一实际上它们每一个都是前一个的超集。
|
||||
@@ -534,7 +529,6 @@ public class MultiCatch {
|
||||
void f() {
|
||||
try {
|
||||
x();
|
||||
|
||||
} catch(Except1 | Except2 | Except3 | Except4 e) {
|
||||
process();
|
||||
}
|
||||
@@ -624,7 +618,7 @@ catch(Exception e) {
|
||||
|
||||
重抛异常会把异常抛给上一级环境中的异常处理程序,同一个 try 块的后续 catch 子句将被忽略。此外,异常对象的所有信息都得以保持,所以高一级环境中捕获此异常的处理程序可以从这个异常对象中得到所有信息。
|
||||
|
||||
如果只是把当前异常对象重新抛出,那么 printStackTrace() 方法显示的将是原来异常抛出点的调用栈信息,而并非重新抛出点的信息。要想更新这个信息,可以调用 filInStackTrace() 方法,这将返回一个 Throwable 对象,它是通过把当前调用栈信息填入原来那个异常对象而建立的,就像这样:
|
||||
如果只是把当前异常对象重新抛出,那么 printStackTrace() 方法显示的将是原来异常抛出点的调用栈信息,而并非重新抛出点的信息。要想更新这个信息,可以调用 fillInStackTrace() 方法,这将返回一个 Throwable 对象,它是通过把当前调用栈信息填入原来那个异常对象而建立的,就像这样:
|
||||
|
||||
```java
|
||||
// exceptions/Rethrowing.java
|
||||
@@ -700,7 +694,7 @@ at Rethrowing.main(Rethrowing.java:38)
|
||||
|
||||
调用 fillInStackTrace() 的那一行就成了异常的新发生地了。
|
||||
|
||||
有可能在捕获异常之后抛出另一种异常。这么做的话,得到的效果类似于使用 filInStackTrace(),有关原来异常发生点的信息会丢失,剩下的是与新的抛出点有关的信息:
|
||||
有可能在捕获异常之后抛出另一种异常。这么做的话,得到的效果类似于使用 fillInStackTrace(),有关原来异常发生点的信息会丢失,剩下的是与新的抛出点有关的信息:
|
||||
|
||||
```java
|
||||
// exceptions/RethrowNew.java
|
||||
@@ -755,7 +749,7 @@ at RethrowNew.main(RethrowNew.java:26)
|
||||
|
||||
### 精准的重新抛出异常
|
||||
|
||||
在 Java 7 之前,如果遇到异常,则只能重新抛出该类型的异常。这导致在 Java 7 中修复的代码不精确。所以在 Java 7 之前,这无法编译:
|
||||
在 Java 7 之前,如果捕捉到一个异常,重新抛出的异常类型只能与原异常完全相同。这导致代码不精确,Java 7修复了这个问题。所以在 Java 7 之前,这无法编译:
|
||||
|
||||
```java
|
||||
class BaseException extends Exception {}
|
||||
@@ -899,14 +893,13 @@ df: d: A new value for d
|
||||
number: 47
|
||||
number2: 48
|
||||
number3: 11
|
||||
|
||||
df.getField("d") : A new value for d
|
||||
DynamicFieldsException
|
||||
at
|
||||
DynamicFields.setField(DynamicFields.java:65)
|
||||
at DynamicFields.main(DynamicFields.java:97)
|
||||
at DynamicFields.setField(DynamicFields.java:65)
|
||||
at DynamicFields.
|
||||
Caused by: java.lang.NullPointerException
|
||||
at
|
||||
DynamicFields.setField(DynamicFields.java:67)
|
||||
at DynamicFields.setField(DynamicFields.java:67)
|
||||
... 1 more
|
||||
```
|
||||
|
||||
@@ -916,7 +909,7 @@ DynamicFields.setField(DynamicFields.java:67)
|
||||
|
||||
你会注意到,toString() 方法使用了一个 StringBuilder 来创建其结果。在 [字符串](./Strings.md) 这章中你将会了解到更多的关于 StringBuilder 的知识,但是只要你编写设计循环的 toString() 方法,通常都会想使用它,就像本例一样。
|
||||
|
||||
主方法中的 catch 子句看起来不同 - 它使用相同的子句处理两种不同类型的异常,并结合“或(|)”符号。此 Java 7 功能有助于减少代码重复,并使你更容易指定要捕获的确切类型,而不是简单地捕获基本类型。你可以通过这种方式组合多种异常类型。
|
||||
`main()` 方法中的 catch 子句看起来不同 - 它使用相同的子句处理两种不同类型的异常,这两种不同的异常通过“或(|)”符号结合起来。 Java 7 的这项功能有助于减少代码重复,并使你更容易指定要捕获的确切类型,而不是简单地捕获一个基类型。你可以通过这种方式组合多种异常类型。
|
||||
|
||||
<!-- Standard Java Exceptions -->
|
||||
|
||||
@@ -924,7 +917,7 @@ DynamicFields.setField(DynamicFields.java:67)
|
||||
|
||||
Throwable 这个 Java 类被用来表示任何可以作为异常被抛出的类。Throwable 对象可分为两种类型(指从 Throwable 继承而得到的类型):Error 用来表示编译时和系统错误(除特殊情况外,一般不用你关心);Exception 是可以被抛出的基本类型,在 Java 类库、用户方法以及运行时故障中都可能抛出 Exception 型异常。所以 Java 程序员关心的基类型通常是 Exception。要想对异常有全面的了解,最好去浏览一下 HTML 格式的 Java 文档(可以从 java.sun.com 下载)。为了对不同的异常有个感性的认识,这么做是值得的。但很快你就会发现,这些异常除了名称外其实都差不多。同时,Java 中异常的数目在持续增加,所以在书中简单罗列它们毫无意义。所使用的第三方类库也可能会有自己的异常。对异常来说,关键是理解概念以及如何使用。
|
||||
|
||||
异常的基本的概念是用名称代表发生的问题,并且异常的名称应该可以望文知意。异常并非全是在 java.lang 包里定义的;有些异常是用来支持其他像 util、net 和 io 这样的程序包,这些异常可以通过它们的完整名称或者从它们的父类中看出端倪。比如,所有的输入/输出异常都是从 java.io.IOException 继承而来的。
|
||||
基本理念是用异常的名称代表发生的问题。异常的名称应该可以望文知意。异常并非全是在 java.lang 包里定义的;有些异常是用来支持其他像 util、net 和 io 这样的程序包,这些异常可以通过它们的完整名称或者从它们的父类中看出端倪。比如,所有的输入/输出异常都是从 java.io.IOException 继承而来的。
|
||||
|
||||
### 特例:RuntimeException
|
||||
|
||||
@@ -937,7 +930,7 @@ if(t == null)
|
||||
|
||||
如果必须对传递给方法的每个引用都检查其是否为 null(因为无法确定调用者是否传入了非法引用),这听起来着实吓人。幸运的是,这不必由你亲自来做,它属于 Java 的标准运行时检测的一部分。如果对 null 引用进行调用,Java 会自动抛出 NullPointerException 异常,所以上述代码是多余的,尽管你也许想要执行其他的检查以确保 NullPointerException 不会出现。
|
||||
|
||||
属于运行时异常的类型有很多,它们会自动被 java 虚拟机抛出,所以不必在异常说明中把它们列出来。这些异常都是从 RuntimeException 类继承而来,所以既体现了继承的优点,使用起来也很方便。这构成了一组具有相同特征和行为的异常类型。并且,也不再需要在异常说明中声明方法将抛出 RuntimeException 类型的异常(或者任何从 RuntimeException 继承的异常),它们也被称为“不受检查异常”。这种异常属于错误,将被自动捕获,就不用你亲自动手了。要是自己去检查 RuntimeException 的话,代码就显得太混乱了。不过尽管通常不用捕获 RuntimeException 异常,但还是可以在代码中抛出 RuntimeException 类型的异常。
|
||||
属于运行时异常的类型有很多,它们被 java 自动抛出,所以不必在异常说明中把它们列出来。非常方便的是,通过将这些异常设置为 `RuntimeException`的子类而把它们归类起来,这是继承的一个绝佳例子:建立具有相同特征和行为的一组类型。
|
||||
|
||||
RuntimeException 代表的是编程错误:
|
||||
|
||||
@@ -980,7 +973,7 @@ at NeverCaught.main(NeverCaught.java:13)
|
||||
|
||||
你会发现,RuntimeException(或任何从它继承的异常)是一个特例。对于这种异常类型,编译器不需要异常说明,其输出被报告给了 System.err。
|
||||
|
||||
请务必记住:只能在代码中忽略 RuntimeException(及其子类)类型的异常,因为所有受检查类型异常的处理都是由编译器强制实施的。
|
||||
请务必记住:代码中只有 RuntimeException(及其子类)类型的异常可以被忽略,因为编译器强制要求处理所有受检查类型的异常。
|
||||
|
||||
值得注意的是:不应把 Java 的异常处理机制当成是单一用途的工具。是的,它被设计用来处理一些烦人的运行时错误,这些错误往往是由代码控制能力之外的因素导致的;然而,它对于发现某些编译器无法检测到的编程错误,也是非常重要的。
|
||||
|
||||
@@ -1040,7 +1033,7 @@ No exception
|
||||
In finally clause
|
||||
```
|
||||
|
||||
可以从输出中发现,无论异常是否被抛出,finally 子句总能被执行。这个程序也给了我们一些思路,当 Java 中的异常不允许我们回到异常抛出的地点时,那么该如何应对呢?如果把 try 块放在循环里,就建立了一个“程序继续执行之前必须要达到”的条件。还可以加入一个 static 类型的计数器或者别的装置,使循环在放弃以前能尝试一定的次数。这将使程序的健壮性更上一个台阶。
|
||||
从输出中发现,无论异常是否被抛出,finally 子句总能被执行。这也为解决 Java 不允许我们回到异常抛出点这一问题,提供了一个思路。如果将 try 块放在循环里,就可以设置一种在程序执行前一定会遇到的异常状况。还可以加入一个 static 类型的计数器或者别的装置,使循环在结束以前能尝试一定的次数。这将使程序的健壮性更上一个台阶。
|
||||
|
||||
### finally 用来做什么?
|
||||
|
||||
@@ -1297,7 +1290,7 @@ public class ExceptionSilencer {
|
||||
|
||||
## 异常限制
|
||||
|
||||
当覆盖方法的时候,只能抛出在基类方法的异常说明里列出的那些异常。这个限制很有用,因为这意味着,若当基类使用的代码应用到其派生类对象的时候,一样能够工作(当然,这是面向对象的基本概念),异常也不例外。
|
||||
当覆盖方法的时候,只能抛出在基类方法的异常说明里列出的那些异常。这个限制很有用,因为这意味着与基类一起工作的代码,也能和导出类一起正常工作(这是面向对象的基本概念),异常也不例外。
|
||||
|
||||
下面例子演示了这种(在编译时)施加在异常上面的限制:
|
||||
|
||||
@@ -1386,11 +1379,11 @@ public class StormyInning extends Inning implements Storm {
|
||||
|
||||
派生类构造器不能捕获基类构造器抛出的异常。
|
||||
|
||||
StormyInning.walk() 不能通过编译是因为它抛出了异常,而 Inning.walk() 并没有声明此异常。如果编译器允许这么做的话,就可以在调用 Inning.walk() 的时候不用做异常处理了,而且当把它替换成 Inning 的派生类的对象时,这个方法就有可能会抛出异常,于是程序就失灵了。通过强制派生类遵守基类方法的异常说明,对象的可替换性得到了保证。
|
||||
StormyInning.walk() 不能通过编译是因为它抛出了一个 Inning.walk() 中没有声明的异常。如果编译器允许这么做的话,就可以编写调用Inning.walk()却不处理任何异常的代码。 但是当使用 `Inning`派生类的对象时,就会抛出异常,从而导致程序出现问题。通过强制派生类遵守基类方法的异常说明,对象的可替换性得到了保证。
|
||||
|
||||
覆盖后的 event() 方法表明,派生类方法可以不抛出任何异常,即使它是基类所定义的异常。同样这是因为,假使基类的方法会抛出异常,这样做也不会破坏已有的程序,所以也没有问题。类似的情况出现在 atBat() 身上,它抛出的是 PopFoul,这个异常是继承自“会被基类的 atBat() 抛出”的 Foul,这样,如果你写的代码是同 Inning 打交道,并且调用了它的 atBat() 的话,那么肯定能捕获 Foul,而 PopFoul 是由 Foul 派生出来的,因此异常处理程序也能捕获 PopFoul。
|
||||
覆盖后的 event() 方法表明,派生类版的方法可以不抛出任何异常,即使基类版的方法抛出了异常。因为这样做不会破坏那些假定基类版的方法会抛出异常的代码。类似的情况出现在 `atBat()`上,它抛出的异常`PopFoul`是由基类版`atBat()`抛出的`Foul` 异常派生而来。如果你写的代码同 `Inning` 一起工作,并且调用了 `atBat()`的话,那么肯定能捕获 `Foul` 。又因为 `PopFoul` 是由 `Foul`派生而来,因此异常处理程序也能捕获 `PopFoul`。
|
||||
|
||||
最后一个值得注意的地方是 main()。这里可以看到,如果处理的刚好是 Stormylnning 对象的话,编译器只会强制要求你捕获这个类所抛出的异常。但是如果将它向上转型成基类型,那么编译器就会(正确地)要求你捕获基类的异常。所有这些限制都是为了能产生更为强壮的异常处理代码。
|
||||
最后一个有趣的地方在 `main()`。如果处理的刚好是 Stormylnning 对象的话,编译器只要求捕获这个类所抛出的异常。但是如果将它向上转型成基类型,那么编译器就会准确地要求捕获基类的异常。所有这些限制都是为了能产生更为健壮的异常处理代码。
|
||||
|
||||
尽管在继承过程中,编译器会对异常说明做强制要求,但异常说明本身并不属于方法类型的一部分,方法类型是由方法的名字与参数的类型组成的。因此,不能基于异常说明来重载方法。此外,一个出现在基类方法的异常说明中的异常,不一定会出现在派生类方法的异常说明里。这点同继承的规则明显不同,在继承中,基类的方法必须出现在派生类里,换句话说,在继承和覆盖的过程中,某个特定方法的“异常说明的接口”不是变大了而是变小了——这恰好和类接口在继承时的情形相反。
|
||||
|
||||
@@ -1571,7 +1564,7 @@ NeedsCleanup 4 disposed
|
||||
- [2] 为了构造和清理,可以看到将具有不能失败的构造器的对象分组在一起。
|
||||
- [3] 展示了如何处理那些具有可以失败的构造器,且需要清理的对象。为了正确处理这种情况,事情变得很棘手,因为对于每一个构造,都必须包含在其自己的 try-finally 语句块中,并且每一个对象构造必须都跟随一个 try-finally 语句块以确保清理。
|
||||
|
||||
本例中的异常处理的棘手程度,对于应该创建不能失败的构造器是一个有力的论据,尽管这么做并非总是可行。
|
||||
本例中异常处理的混乱情形,有力的论证了应该创建不会抛出异常的构造器,尽管这并不总会实现。
|
||||
|
||||
注意,如果 dispose() 可以抛出异常,那么你可能需要额外的 try 语句块。基本上,你应该仔细考虑所有的可能性,并确保正确处理每一种情况。
|
||||
|
||||
@@ -1581,7 +1574,7 @@ NeedsCleanup 4 disposed
|
||||
|
||||
上一节的内容可能让你有些头疼。在考虑所有可能失败的方法时,找出放置所有 try-catch-finally 块的位置变得令人生畏。确保没有任何故障路径,使系统远离不稳定状态,这非常具有挑战性。
|
||||
|
||||
InputFile.java 是一个特别棘手的情况,因为文件被打开(包含所有可能的异常),然后它在对象的生命周期中保持打开状态。每次调用 getLine() 都会导致异常,因此可以调用 dispose() 方法。这是一个很好的例子,因为它显示了事物的混乱程度。它还表明你应该尝试最好不要那样设计代码(当然,你经常会遇到这种你无法选择的代码设计的情况,因此你必须仍然理解它)。
|
||||
`InputFile.java` 是一个特别棘手的情况,因为文件被打开(伴随所有可能因此产生的异常),然后它在对象的生命周期中保持打开状态。每次调用 `getLine()`都可能导致异常,而且 `dispose()`也是这种情况。这个例子只是好在它显示了事情可以混乱到什么地步。它还表明了你应该尽量不要那样设计代码(当然,你经常会遇到这种无法选择的代码设计的情况,因此你仍然必须要理解它)。
|
||||
|
||||
InputFile.java 一个更好的实现方式是如果构造函数读取文件并在内部缓冲它 —— 这样,文件的打开,读取和关闭都发生在构造函数中。或者,如果读取和存储文件不切实际,你可以改为生成 Stream。理想情况下,你可以设计成如下的样子:
|
||||
|
||||
@@ -1624,7 +1617,7 @@ main(String[] args) throws IOException {
|
||||
1. 需要资源清理
|
||||
2. 需要在特定的时刻进行资源清理,比如你离开作用域的时候(在通常情况下意味着通过异常进行清理)。
|
||||
|
||||
一个常见的例子是 jav.io.FileInputstream(将会在 [附录:I/O 流 ](./Appendix-IO-Streams.md) 中提到)。要正确使用它,你必须编写一些棘手的样板代码:
|
||||
一个常见的例子是 `java.io.FileInputStream` (将会在 [附录:I/O 流 ](./Appendix-IO-Streams.md) 中提到)。要正确使用它,你必须编写一些棘手的样板代码:
|
||||
|
||||
```java
|
||||
// exceptions/MessyExceptions.java
|
||||
@@ -1674,9 +1667,9 @@ public class TryWithResources {
|
||||
}
|
||||
```
|
||||
|
||||
在 Java 7 之前,try 总是后面跟着一个 {,但是现在可以跟一个带括号的定义 - 这里是我们创建的 FileInputStream 对象。括号内的部分称为资源规范头(resource specification header)。现在可用于整个 try 块的其余部分。更重要的是,无论你如何退出 try 块(正常或异常),都会执行前一个 finally 子句的等价物,但不会编写那些杂乱而棘手的代码。这是一项重要的改进。
|
||||
在 Java 7 之前,try 后面总是跟着一个 {,但是现在可以跟一个带括号的定义 ——这里是我们创建的 FileInputStream 对象。括号内的部分称为资源规范头(resource specification header)。现在 `in` 在整个 try 块的其余部分都是可用的。更重要的是,无论你如何退出 try 块(正常或通过异常),和以前的 finally 子句等价的代码都会被执行,并且不用编写那些杂乱而棘手的代码。这是一项重要的改进。
|
||||
|
||||
它是如何工作的?在 try-with-resources 定义子句中创建的对象(在括号内)必须实现 java.lang.AutoCloseable 接口,这个接口有一个方法:close()。当在 Java 7 中引入 AutoCloseable 时,许多接口和类被修改以实现它;查看 Javadocs 中的 AutoCloseable,可以找到所有实现该接口的类列表,其中包括 Stream 对象:
|
||||
它是如何工作的? try-with-resources 定义子句中创建的对象(在括号内)必须实现 `java.lang.AutoCloseable` 接口,这个接口只有一个方法:`close()`。当在 Java 7 中引入 `AutoCloseable` 时,许多接口和类被修改以实现它;查看 Javadocs 中的 AutoCloseable,可以找到所有实现该接口的类列表,其中包括 `Stream` 对象:
|
||||
|
||||
```java
|
||||
// exceptions/StreamsAreAutoCloseable.java
|
||||
@@ -1743,7 +1736,7 @@ Closing Second
|
||||
Closing First
|
||||
```
|
||||
|
||||
退出 try 块会调用两个对象的 close() 方法,并以与创建顺序相反的顺序关闭它们。顺序很重要,因为在此配置中,Second 对象可能依赖于 First 对象,因此如果 First 在第 Second 关闭时已经关闭。 Second 的 close() 方法可能会尝试访问 First 中不再可用的某些功能。
|
||||
退出 try 块会调用两个对象的 close() 方法,并以与创建顺序相反的顺序关闭它们。顺序很重要,因为在这种情况下,Second 对象可能依赖于 First 对象,因此如果 First 在第 Second 关闭时已经关闭。 Second 的 close() 方法可能会尝试访问 First 中不再可用的某些功能。
|
||||
|
||||
假设我们在资源规范头中定义了一个不是 AutoCloseable 的对象
|
||||
|
||||
@@ -1802,7 +1795,7 @@ Caught: CE
|
||||
|
||||
正如预期的那样,First 创建时没有发生意外,SecondExcept 在创建期间抛出异常。请注意,不会为 SecondExcept 调用 close(),因为如果构造函数失败,则无法假设你可以安全地对该对象执行任何操作,包括关闭它。由于 SecondExcept 的异常,Second 对象实例 s2 不会被创建,因此也不会有清除事件发生。
|
||||
|
||||
如果没有构造函数抛出异常,但你可能会在 try 的主体中获取它们,则再次强制你实现 catch 子句:
|
||||
如果没有构造函数抛出异常,但在 try 的主体中可能抛出异常,那么你将再次被强制要求提供一个catch 子句:
|
||||
|
||||
```java
|
||||
// exceptions/BodyException.java
|
||||
@@ -1952,10 +1945,9 @@ try {
|
||||
|
||||
## 其他可选方式
|
||||
|
||||
异常处理系统就像一个活门(trap door),使你能放弃程序的正常执行序列。当“异常情形”
|
||||
发生的时候,正常的执行已变得不可能或者不需要了,这时就要用到这个“活门"。异常代表了当前方法不能继续执行的情形。开发异常处理系统的原因是,如果为每个方法所有可能发生的错误都进行处理的话,任务就显得过于繁重了,程序员也不愿意这么做。结果常常是将错误忽略。应该注意到,开发异常处理的初衷是为了方便程序员处理错误。
|
||||
异常处理系统就像一个活门(trap door),使你能放弃程序的正常执行序列。当“异常情形”发生的时候,正常的执行已变得不可能或者不需要了,这时就要用到这个“活门"。异常代表了当前方法不能继续执行的情形。开发异常处理系统的原因是,如果为每个方法所有可能发生的错误都进行处理的话,任务就显得过于繁重了,程序员也不愿意这么做。结果常常是将错误忽略。应该注意到,开发异常处理的初衷是为了方便程序员处理错误。
|
||||
|
||||
异常处理的一个重要原则是“只有在你知道如何处理的情况下才捕获异常"。实际上,异常处理的一个重要目标就是把错误处理的代码同错误发生的地点相分离。这使你能在一段代码中专注于要完成的事情,至于如何处理错误,则放在另一段代码中完成。这样一来,主要代码就不会与错误处理逻辑混在一起,也更容易理解和维护。通过允许一个处理程序去处理多个出错点,异常处理还使得错误处理代码的数量趋于减少。
|
||||
异常处理的一个重要原则是“只有在你知道如何处理的情况下才捕获异常"。实际上,异常处理的一个重要目标就是把处理错误的代码同错误发生的地点相分离。这使你能在一段代码中专注于要完成的事情,至于如何处理错误,则放在另一段代码中完成。这样一来,主要代码就不会与错误处理逻辑混在一起,也更容易理解和维护。通过允许一个处理程序去处理多个出错点,异常处理还使得错误处理代码的数量趋于减少。
|
||||
|
||||
“被检查的异常”使这个问题变得有些复杂,因为它们强制你在可能还没准备好处理错误的时候被迫加上 catch 子句,这就导致了吞食则有害(harmful if swallowed)的问题:
|
||||
|
||||
@@ -1969,7 +1961,8 @@ try {
|
||||
|
||||
当我意识到犯了这么大一个错误时,简直吓了一大跳,在本书第 2 版中,我在处理程序里通过打印栈轨迹的方法“修补”了这个问题(本章中的很多例子还是使用了这种方法,看起来还是比较合适的),虽然这样可以跟踪异常的行为,但是仍旧不知道该如何处理异常。这一节,我们来研究一下“被检查的异常”及其并发症,以及采用什么方法来解决这些问题。
|
||||
|
||||
这个话题看起来简单,但实际上它不仅复杂,更重要的是还非常多变。总有人会顽固地坚持自己的立场,声称正确答案(也是他们的答案)是显而易见的。我觉得之所以会有这种观点,是因为我们使用的工具已经不是 ANS1 标准出台前的像 C 那样的弱类型语言,而是像 C++ 和 Java 这样的“强静态类型语言”(也就是编译时就做类型检查的语言),这是前者所无法比拟的。当刚开始这种转变的时候(就像我一样),会觉得它带来的好处是那样明显,好像类型检查总能解决所有的问题。在此,我想结合我自己的认识过程,告诉读者我是怎样从对类型检查的绝对迷信变成持怀疑态度的,当然,很多时候它还是非常有用的,但是当它挡住我们的去路并成为障碍的时候,我们就得跨过去。只是这条界限往往并不是很清晰(我最喜欢的一句格言是:所有模型都是错误的,但有些是能用的)。
|
||||
这个话题看起来简单,但实际上它不仅复杂,更重要的是还非常多变。总有人会顽固地坚持自己的立场,声称正确答案(也是他们的答案)是显而易见的。我觉得之所以会有这种观点,是因为我们使用的工具已经不是 ANSI 标准出台前的像 C 那样的弱类型语言,而是像 C++ 和 Java 这样的“强静态类型语言”(也就是编译时就做类型检查的语言),这是前者所无法比拟的。当刚开始这种转变的时候(就像我一样),会觉得它带来的好处是那样明显,好像类型检查总能解决所有的问题。在此,我想结合我自己的认识过程,告诉读者我是怎样从对类型检查的绝对迷信变成持怀疑态度的,当然,很多时候它还是非常有用的,但是当它挡住我们的去路并成为障碍的时候,我们就得跨过去。只是这条界限往往并不是很清晰(我最喜欢的一句格言是:所有模型都是错误的,但有些是能用的)。
|
||||
|
||||
|
||||
### 历史
|
||||
|
||||
@@ -1995,7 +1988,7 @@ C++ 的异常说明不属于函数的类型信息。编译时唯一要检查的
|
||||
|
||||
### 观点
|
||||
|
||||
首先,Java 无谓地发明了“被检查的异常”(很明显是受 C++ 异常说明的启发,以及受 C++ 程序员们一般对此无动于衷的事实的影响),但是,这还只是一次尝试,目前为止还没有别的语言采用这种做法。
|
||||
首先,值得注意的是 Java 有效的发明了“被检查的异常”(很明显是受 C++ 异常说明的启发,以及异常说明通常并不烦扰 C++ 程序员的事实),但是,这还只是一次尝试,目前还没有别的语言选择复制这种做法。
|
||||
|
||||
其次,仅从示意性的例子和小程序来看,“被检查的异常”的好处很明显。但是当程序开始变大的时候,就会带来一些微妙的问题。当然,程序不是一下就变大的,这有个过程。如果把不适用于大项目的语言用于小项目,当这些项目不断膨胀时,突然有一天你会发现,原来可以管理的东西,现在已经变得无法管理了。这就是我所说的过多的类型检查,特别是“被检查的异常"所造成的问题。
|
||||
|
||||
@@ -2018,6 +2011,8 @@ Martin Fowler(UML Distilled,Refactoring 和 Analysis Patterns 的作者)
|
||||
|
||||
> “...总体来说,我觉得异常很不错,但是 Java 的”被检查的异常“带来的麻烦比好处要多。”
|
||||
|
||||
我现在认为 Java 的重要进步是统一了错误报告模式,所有错误都用异常来报告。这没有在 C++ 中发生,原因是为了向后兼容 C ,直接忽略错误的旧模式在 C++ 中依然是可用的。如果想使用异常,可以始终用异常来报告,如果不想这样做,异常可以抛到最高的级别(比如控制台)。当 Java 修改 C++ 的模式来让异常成为报告错误的唯一方式时,受检查的异常的额外限制可能就变得不那么必要了。
|
||||
|
||||
过去,我曾坚定地认为“被检查的异常”和强静态类型检查对开发健壮的程序是非常必要的。但是,我看到的以及我使用一些动态(类型检查)语言的亲身经历告诉我,这些好处实际上是来自于:
|
||||
|
||||
1. 不在于编译器是否会强制程序员去处理错误,而是要有一致的、使用异常来报告错误的模型。
|
||||
@@ -2033,7 +2028,7 @@ Martin Fowler(UML Distilled,Refactoring 和 Analysis Patterns 的作者)
|
||||
|
||||
### 把异常传递给控制台
|
||||
|
||||
对于简单的程序,比如本书中的许多例子,最简单而又不用写多少代码就能保护异常信息的方法,就是把它们从 main() 传递到控制台。例如,为了读取信息而打开一个文件(在第 12 章将详细介绍),必须对 FilelnputStream 进行打开和关闭操作,这就可能会产生异常。对于简单的程序,可以像这样做(本书中很多地方采用了这种方法):
|
||||
在简单的程序中,不用写多少代码就能保留异常的最简单的方法,就是把它们从 `main()` 传递到控制台。例如,为了读取信息而打开一个文件(在[文件](17-Files.md) 章节中将详细介绍),必须对 `FilelnputStream` 进行打开和关闭操作,这就可能会产生异常。对于简单的程序,可以像这样做(本书中很多地方采用了这种方法):
|
||||
|
||||
```java
|
||||
// exceptions/MainException.java
|
||||
@@ -2054,9 +2049,7 @@ public class MainException {
|
||||
|
||||
### 把“被检查的异常”转换为“不检查的异常”
|
||||
|
||||
在编写你自己使用的简单程序时,从主方法中抛出异常是很方便的,但这不是通用的方法。
|
||||
|
||||
问题的实质是,当在一个普通方法里调用别的方法时,要考虑到“我不知道该这样处理这个异常,但是也不想把它‘吞’了,或者打印一些无用的消息”。异常链提供了一种新的思路来解决这个问题。可以直接把“被检查的异常”包装进 RuntimeException 里面,就像这样:
|
||||
当编写自己使用的简单程序时,从 `main()` 中抛出异常是很方便的,但这并不总是有用。真正的问题是,当在一个普通方法里调用别的方法时发现:“我不知道该如何处理这个异常,但是不能把它'吞掉'或者打印一些无用的消息。”有了异常链,一个简单的解决办法就出现了。可以通过将一个“被检查的异常”传递给`RuntimeException` 的构造器,从而将它包装进 `RuntimeException` 里,就像这样:
|
||||
|
||||
```java
|
||||
try {
|
||||
@@ -2134,11 +2127,11 @@ Throwable: java.lang.RuntimeException: Where am I?
|
||||
SomeOtherException: SomeOtherException
|
||||
```
|
||||
|
||||
WrapCheckedException.throwRuntimeException() 的代码可以生成不同类型的异常。这些异常被捕获并包装进了 RuntimeException 对象,所以它们成了这些运行时异常的"cause"了。
|
||||
`WrapCheckedException.throwRuntimeException()` 包含可生成不同类型异常的代码。这些异常被捕获并包装进`RuntimeException` 对象,所以它们成了这些运行时异常的原因("cause")。
|
||||
|
||||
在 TurnOfChecking 里,可以不用 try 块就调用 throwRuntimeException(),因为它没有抛出“被检查的异常”。但是,当你准备好去捕获异常的时候,还是可以用 try 块来捕获任何你想捕获的异常的。应该捕获 try 块肯定会抛出的异常,这里就是 SomeOtherException,RuntimeException 要放到最后去捕获。然后把 getCause() 的结果(也就是被包装的那个原始异常)抛出来。这样就把原先的那个异常给提取出来了,然后就可以用它们自己的 catch 子句进行处理。
|
||||
|
||||
本书余下部分将会在合适的时候使用这种“用 RuntimeException 来包装,被检查的异常”的技术。另一种解决方案是创建自己的 RuntimeException 的子类。在这种方式中,不必捕获它,但是希望得到它的其他代码都可以捕获它。
|
||||
这种把被检查的异常用 `RuntimeException` 包装起来的技术,将在本书余下部分使用。另一种解决方案是创建自己的 `RuntimeException` 的子类。这样的话,异常捕获将不被强制要求,但是任何人都可以在需要的时候捕获这些异常。
|
||||
|
||||
<!-- Exception Guidelines -->
|
||||
|
||||
@@ -2171,11 +2164,11 @@ WrapCheckedException.throwRuntimeException() 的代码可以生成不同类型
|
||||
|
||||
(来自于 2011 年的一篇博文)
|
||||
|
||||
我的朋友 James Ward 正在尝试使用 JDBC 创建一些非常简单的教学示例,并且不断被检查的异常所挫败。他向我指出 Howard Lewis Ship 的帖子“[被检查的例外的悲剧](http://tapestryjava.blogspot.com/2011/05/tragedy-of-checked-exceptions.html)”。特别是。James 对他必须跳过去做一些应该简单的事情的所有环感到沮丧。即使在 finally 块中,他也不得不放入更多的 try-catch 子句,因为关闭连接也会导致异常。它在哪里结束?为了简单起见,你必须在环之后跳过环(请注意,try-with-resources语句可以显著改善这种情况)。
|
||||
我的朋友 James Ward 正在尝试使用 JDBC 创建一些非常简单的教学示例,但不断被受检查的异常所挫败。他把 Howard Lewis Ship 的帖子“[被检查的异常的悲剧](http://tapestryjava.blogspot.com/2011/05/tragedy-of-checked-exceptions.html)”指给我看。让 James 尤其沮丧的是,即使做一些本来很简单的事情,也必须在一个个环里跳来跳去。即使在 `finally` 块中,他也不得不放入更多的 `try-catch` 子句,因为关闭连接也会导致异常。这些麻烦事的终点在哪里?本来只是做一些简单的事,但却被强制要求在一个个环里跳来跳去(注意,try-with-resources语句可以显著改善这种情况)。
|
||||
|
||||
我们开始讨论 Go 编程语言,我很着迷,因为Rob Pike等人。我们已经清楚地提出了许多关于语言设计的非常尖锐和基本的问题。基本上,他们已经采取了我们开始接受的有关语言的所有内容,并询问“为什么?”关于每一种语言。学习这门语言真的让你思考和怀疑。
|
||||
|
||||
我的印象是,Go团队决定不做任何假设,只有在明确需要特征的情况下才能改进语言。他们似乎并不担心进行破坏旧代码的更改 - 他们创建了一个重写工具,因此如果他们进行了这些更改,它将为您重写代码。这使他们能够使语言成为一个持续的实验,以发现真正需要的东西,而不是做 Big Upfront Design。
|
||||
我的印象是Go团队不做任何臆断,只有在明确一个特征是必须的时候才改进语言。他们似乎并不担心做出破坏旧代码的更改 ,因为他们创建了一个重写工具,当做出更改的时候,重写工具将为你重写代码。这使他们将语言变成一个前进的实验,以发现真正需要的东西,而不是做 Big Upfront Design。
|
||||
|
||||
他们做出的最有趣的决定之一是完全排除异常。你没有看错 —— 他们不只是遗漏了经过检查的异常情况。他们遗漏了所有异常情况。
|
||||
|
||||
@@ -2185,11 +2178,11 @@ WrapCheckedException.throwRuntimeException() 的代码可以生成不同类型
|
||||
result, err := functionCall()
|
||||
```
|
||||
|
||||
( := 告诉 Go 语言这里定义 result 和 err,并且推断他们的数据类型)
|
||||
( `:=` 告诉 Go 语言在这里定义 `result` 和 `err`,并且推断它们的类型)
|
||||
|
||||
就是这样:对于每次调用,您都会获得结果对象和错误对象。您可以立即检查错误(这是典型的,因为如果某些操作失败,则不太可能继续下一步),或者稍后检查是否有效。
|
||||
|
||||
起初这似乎很原始,是古代的回归。但到目前为止,我发现 Go 中的决定都得到了很好的考虑,值得深思。我只是做出反应,因为我的大脑是异常的吗?这会如何影响 James 的问题?
|
||||
起初这看起来很原始,是向“古代”的回归。但到目前为止,我发现 Go 中的决定都经过了很好的考虑,值得深思。我的反应是因为我的大脑是异常的吗?这会如何影响 James 的问题?
|
||||
|
||||
它发生在我身上,我已经将异常处理视为一种并行执行路径。如果你遇到异常,你会跳出正常的路径进入这个并行执行路径,这是一种“奇异世界”,你不再做你写的东西,而是跳进 catch 和 finally 子句。正是这种替代执行路径的世界导致了 James 抱怨的问题。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user