提交 6906c750 编写于 作者: W wizardforcel

ch9

上级 530c7d98
# 9.10 练习
(1) 用main()创建一个类,令其抛出try块内的Exception类的一个对象。为Exception的构造器赋予一个字符串参数。在catch从句内捕获异常,并打印出字符串参数。添加一个finally从句,并打印一条消息,证明自己真正到达那里。
(1) 用`main()`创建一个类,令其抛出`try`块内的`Exception`类的一个对象。为`Exception`的构造器赋予一个字符串参数。在`catch`从句内捕获异常,并打印出字符串参数。添加一个`finally`从句,并打印一条消息,证明自己真正到达那里。
(2) 用extends关键字创建自己的异常类。为这个类写一个构造器,令其采用String参数,并随同String引用把它保存到对象内。写一个方法,令其打印出保存下来的String。创建一个try-catch从句,练习实际操作新异常。
(2) 用`extends`关键字创建自己的异常类。为这个类写一个构造器,令其采用`String`参数,并随同`String`引用把它保存到对象内。写一个方法,令其打印出保存下来的`String`。创建一个`try-catch`从句,练习实际操作新异常。
(3) 写一个类,并令一个方法抛出在练习2中创建的类型的一个异常。试着在没有异常规范的前提下编译它,观察编译器会报告什么。接着添加适当的异常规范。在一个try-catch从句中尝试自己的类以及它的异常。
(3) 写一个类,并令一个方法抛出在练习2中创建的类型的一个异常。试着在没有异常规范的前提下编译它,观察编译器会报告什么。接着添加适当的异常规范。在一个`try-catch`从句中尝试自己的类以及它的异常。
(4) 在第5章,找到调用了Assert.java的两个程序,并修改它们,令其抛出自己的异常类型,而不是打印到System.err。该异常应是扩展了RuntimeException的一个内部类。
\ No newline at end of file
(4) 在第5章,找到调用了`Assert.java`的两个程序,并修改它们,令其抛出自己的异常类型,而不是打印到`System.err`。该异常应是扩展了`RuntimeException`的一个内部类。
......@@ -225,7 +225,7 @@ java.lang.Exception: thrown from f()
由于使用的是`fillInStackTrace()`,第18行成为异常的新起点。
针对`g()``main()``Throwable`类必须在异常规中出现,因为`fillInStackTrace()`会生成一个`Throwable`对象的引用。由于`Throwable``Exception`的一个基础类,所以有可能获得一个能够“抛”出的对象(具有`Throwable`属性),但却并非一个`Exception`(异常)。因此,在`main()`中用于`Exception`的引用可能丢失自己的目标。为保证所有东西均井然有序,编译器强制`Throwable`使用一个异常规范。举个例子来说,下述程序的异常便不会在`main()`中被捕获到:
针对`g()``main()``Throwable`类必须在异常规中出现,因为`fillInStackTrace()`会生成一个`Throwable`对象的引用。由于`Throwable``Exception`的一个基础类,所以有可能获得一个能够“抛”出的对象(具有`Throwable`属性),但却并非一个`Exception`(异常)。因此,在`main()`中用于`Exception`的引用可能丢失自己的目标。为保证所有东西均井然有序,编译器强制`Throwable`使用一个异常规范。举个例子来说,下述程序的异常便不会在`main()`中被捕获到:
```
//: ThrowOut.java
......
# 9.3 标准Java异常
Java包含了一个名为Throwable的类,它对可以作为异常“抛”出的所有东西进行了描述。Throwable对象有两种常规类型(亦即“从Throwable继承”)。其中,Error代表编译期和系统错误,我们一般不必特意捕获它们(除在特殊情况以外)。Exception是可以从任何标准Java库的类方法中“抛”出的基本类型。此外,它们亦可从我们自己的方法以及运行期偶发事件中“抛”出。
Java包含了一个名为`Throwable`的类,它对可以作为异常“抛”出的所有东西进行了描述。`Throwable`对象有两种常规类型(亦即“从`Throwable`继承”)。其中,`Error`代表编译期和系统错误,我们一般不必特意捕获它们(除在特殊情况以外)。`Exception`是可以从任何标准Java库的类方法中“抛”出的基本类型。此外,它们亦可从我们自己的方法以及运行期偶发事件中“抛”出。
为获得异常的一个综合概念,最好的方法是阅读由http://java.sun.com提供的联机Java文档(当然,首先下载它们更好)。为了对各种异常有一个大概的印象,这个工作是相当有价值的。但大家不久就会发现,除名字外,一个异常和下一个异常之间并不存在任何特殊的地方。此外,Java提供的异常数量正在日益增多;从本质上说,把它们印到一本书里是没有意义的。大家从其他地方获得的任何新库可能也提供了它们自己的异常。我们最需要掌握的是基本概念,以及用这些异常能够做什么。
为获得异常的一个综合概念,最好的方法是阅读由`http://java.sun.com`提供的联机Java文档(当然,首先下载它们更好)。为了对各种异常有一个大概的印象,这个工作是相当有价值的。但大家不久就会发现,除名字外,一个异常和下一个异常之间并不存在任何特殊的地方。此外,Java提供的异常数量正在日益增多;从本质上说,把它们印到一本书里是没有意义的。大家从其他地方获得的任何新库可能也提供了它们自己的异常。我们最需要掌握的是基本概念,以及用这些异常能够做什么。
```
java.lang.Exception
```
这是程序能捕获的基本异常。其他异常都是从它衍生出去的。这里要注意的是异常的名字代表发生的问题,而且异常名通常都是精心挑选的,可以很清楚地说明到底发生了什么事情。异常并不全是在java.lang中定义的;有些是为了提供对其他库的支持,如util,net以及io等——我们可以从它们的完整类名中看出这一点,或者观察它们从什么继承。例如,所有IO异常都是从java.io.IOException继承的。
这是程序能捕获的基本异常。其他异常都是从它衍生出去的。这里要注意的是异常的名字代表发生的问题,而且异常名通常都是精心挑选的,可以很清楚地说明到底发生了什么事情。异常并不全是在`java.lang`中定义的;有些是为了提供对其他库的支持,如`util``net`以及`io`等——我们可以从它们的完整类名中看出这一点,或者观察它们从什么继承。例如,所有`IO`异常都是从`java.io.IOException`继承的。
9.3.1 RuntimeException的特殊情况
9.3.1 `RuntimeException`的特殊情况
本章的第一个例子是:
......@@ -20,11 +20,11 @@ if(t == null)
throw new NullPointerException();
```
看起来似乎在传递进入一个方法的每个引用中都必须检查null(因为不知道调用者是否已传递了一个有效的引用),这无疑是相当可怕的。但幸运的是,我们根本不必这样做——它属于Java进行的标准运行期检查的一部分。若对一个空引用发出了调用,Java会自动产生一个NullPointerException异常。所以上述代码在任何情况下都是多余的。
看起来似乎在传递进入一个方法的每个引用中都必须检查`null`(因为不知道调用者是否已传递了一个有效的引用),这无疑是相当可怕的。但幸运的是,我们根本不必这样做——它属于Java进行的标准运行期检查的一部分。若对一个空引用发出了调用,Java会自动产生一个`NullPointerException`异常。所以上述代码在任何情况下都是多余的。
这个类别里含有一系列异常类型。它们全部由Java自动生成,毋需我们亲自动手把它们包含到自己的异常规范里。最方便的是,通过将它们置入单独一个名为RuntimeException的基础类下面,它们全部组合到一起。这是一个很好的继承例子:它建立了一系列具有某种共通性的类型,都具有某些共通的特征与行为。此外,我们没必要专门写一个异常规范,指出一个方法可能会“抛”出一个RuntimeException,因为已经假定可能出现那种情况。由于它们用于指出编程中的错误,所以几乎永远不必专门捕获一个“运行期异常”——RuntimeException——它在默认情况下会自动得到处理。若必须检查RuntimeException,我们的代码就会变得相当繁复。在我们自己的包里,可选择“抛”出一部分RuntimeException
这个类别里含有一系列异常类型。它们全部由Java自动生成,毋需我们亲自动手把它们包含到自己的异常规范里。最方便的是,通过将它们置入单独一个名为`RuntimeException`的基础类下面,它们全部组合到一起。这是一个很好的继承例子:它建立了一系列具有某种共通性的类型,都具有某些共通的特征与行为。此外,我们没必要专门写一个异常规范,指出一个方法可能会“抛”出一个`RuntimeException`,因为已经假定可能出现那种情况。由于它们用于指出编程中的错误,所以几乎永远不必专门捕获一个“运行期异常”——`RuntimeException`——它在默认情况下会自动得到处理。若必须检查`RuntimeException`,我们的代码就会变得相当繁复。在我们自己的包里,可选择“抛”出一部分`RuntimeException`
如果不捕获这些异常,又会出现什么情况呢?由于编译器并不强制异常规范捕获它们,所以假如不捕获的话,一个RuntimeException可能过滤掉我们到达main()方法的所有途径。为体会此时发生的事情,请试试下面这个例子:
如果不捕获这些异常,又会出现什么情况呢?由于编译器并不强制异常规范捕获它们,所以假如不捕获的话,一个`RuntimeException`可能过滤掉我们到达`main()`方法的所有途径。为体会此时发生的事情,请试试下面这个例子:
```
//: NeverCaught.java
......@@ -43,7 +43,7 @@ public class NeverCaught {
} ///:~
```
大家已经看到,一个RuntimeException(或者从它继承的任何东西)属于一种特殊情况,因为编译器不要求为这些类型指定异常规范。
大家已经看到,一个`RuntimeException`(或者从它继承的任何东西)属于一种特殊情况,因为编译器不要求为这些类型指定异常规范。
输出如下:
......@@ -54,13 +54,13 @@ at NeverCaught.g(NeverCaught.java:12)
at NeverCaught.main(NeverCaught.java:15)
```
所以答案就是:假若一个RuntimeException获得到达main()的所有途径,同时不被捕获,那么当程序退出时,会为那个异常调用printStackTrace()
所以答案就是:假若一个`RuntimeException`获得到达`main()`的所有途径,同时不被捕获,那么当程序退出时,会为那个异常调用`printStackTrace()`
注意也许能在自己的代码中仅忽略RuntimeException,因为编译器已正确实行了其他所有控制。因为RuntimeException在此时代表一个编程错误:
注意也许能在自己的代码中仅忽略`RuntimeException`,因为编译器已正确实行了其他所有控制。因为`RuntimeException`在此时代表一个编程错误:
(1) 一个我们不能捕获的错误(例如,由客户程序员接收传递给自己方法的一个空引用)。
(2) 作为一名程序员,一个应在自己的代码中检查的错误(如ArrayIndexOutOfBoundException,此时应注意数组的大小)。
(2) 作为一名程序员,一个应在自己的代码中检查的错误(如`ArrayIndexOutOfBoundException`,此时应注意数组的大小)。
可以看出,最好的做法是在这种情况下异常,因为它们有助于程序的调试。
另外一个有趣的地方是,我们不可将Java异常划分为单一用途的工具。的确,它们设计用于控制那些讨厌的运行期错误——由代码控制范围之外的其他力量产生。但是,它也特别有助于调试某些特殊类型的编程错误,那些是编译器侦测不到的。
......@@ -52,7 +52,7 @@ class MyException extends Exception {
}
```
这里的关键是“extends Exception”,它的意思是:除包括一个Exception的全部含义以外,还有更多的含义。增加的代码数量非常少——实际只添加了两个构造器,对MyException的创建方式进行了定义。请记住,假如我们不明确调用一个基础类构造器,编译器会自动调用基础类默认构造器。在第二个构造器中,通过使用super关键字,明确调用了带有一个String参数的基础类构造器。
这里的关键是`extends Exception`,它的意思是:除包括一个`Exception`的全部含义以外,还有更多的含义。增加的代码数量非常少——实际只添加了两个构造器,对`MyException`的创建方式进行了定义。请记住,假如我们不明确调用一个基础类构造器,编译器会自动调用基础类默认构造器。在第二个构造器中,通过使用`super`关键字,明确调用了带有一个`String`参数的基础类构造器。
该程序输出结果如下:
......@@ -67,7 +67,7 @@ MyException: Originated in g()
at Inheriting.main(Inheriting.java:29)
```
可以看到,在从f()“抛”出的MyException异常中,缺乏详细的消息。
可以看到,在从`f()`“抛”出的`MyException`异常中,缺乏详细的消息。
创建自己的异常时,还可以采取更多的操作。我们可添加额外的构造器及成员:
......@@ -126,7 +126,7 @@ public class Inheriting2 {
} ///:~
```
此时添加了一个数据成员i;同时添加了一个特殊的方法,用它读取那个值;也添加了一个额外的构造器,用它设置那个值。输出结果如下:
此时添加了一个数据成员`i`;同时添加了一个特殊的方法,用它读取那个值;也添加了一个额外的构造器,用它设置那个值。输出结果如下:
```
Throwing MyException2 from f()
......@@ -152,4 +152,4 @@ class SimpleException extends Exception {
} ///:~
```
它要依赖编译器来创建默认构造器(会自动调用基础类的默认构造器)。当然,在这种情况下,我们不会得到一个SimpleException(String)构造器,但它实际上也不会经常用到。
它要依赖编译器来创建默认构造器(会自动调用基础类的默认构造器)。当然,在这种情况下,我们不会得到一个`SimpleException(String)`构造器,但它实际上也不会经常用到。
......@@ -6,9 +6,9 @@
```
//: StormyInning.java
// Overridden methods may throw only the
// exceptions specified in their base-class
// versions, or exceptions derived from the
// Overridden methods may throw only the
// exceptions specified in their base-class
// versions, or exceptions derived from the
// base-class exceptions.
class BaseballException extends Exception {}
......@@ -33,14 +33,14 @@ interface Storm {
void rainHard() throws RainedOut;
}
public class StormyInning extends Inning
public class StormyInning extends Inning
implements Storm {
// OK to add new exceptions for constructors,
// but you must deal with the base constructor
// exceptions:
StormyInning() throws RainedOut,
StormyInning() throws RainedOut,
BaseballException {}
StormyInning(String s) throws Foul,
StormyInning(String s) throws Foul,
BaseballException {}
// Regular methods must conform to base class:
//! void walk() throws PopFoul {} //Compile error
......@@ -53,7 +53,7 @@ public class StormyInning extends Inning
// You can choose to not throw any exceptions,
// even if base version does:
public void event() {}
// Overridden methods can throw
// Overridden methods can throw
// inherited exceptions:
void atBat() throws PopFoul {}
public static void main(String[] args) {
......@@ -78,17 +78,17 @@ public class StormyInning extends Inning
} ///:~
```
Inning中,可以看到无论构造器还是event()方法都指出自己会“抛”出一个异常,但它们实际上没有那样做。这是合法的,因为它允许我们强迫用户捕获可能在覆盖过的event()版本里添加的任何异常。同样的道理也适用于abstract方法,就象在atBat()里展示的那样。
`Inning`中,可以看到无论构造器还是`event()`方法都指出自己会“抛”出一个异常,但它们实际上没有那样做。这是合法的,因为它允许我们强迫用户捕获可能在覆盖过的event()版本里添加的任何异常。同样的道理也适用于`abstract`方法,就象在`atBat()`里展示的那样。
“interface Storm”非常有趣,因为它包含了在Incoming中定义的一个方法——event(),以及不是在其中定义的一个方法。这两个方法都会“抛”出一个新的异常类型:RainedOut。当执行到“StormyInning extends”和“implements Storm”的时候,可以看到Storm中的event()方法不能改变Inning中的event()的异常接口。同样地,这种设计是十分合理的;否则的话,当我们操作基础类时,便根本无法知道自己捕获的是否正确的东西。当然,假如interface中定义的一个方法不在基础类里,比如rainHard(),它产生异常时就没什么问题。
`interface Storm`非常有趣,因为它包含了在`Incoming`中定义的一个方法——`event()`,以及不是在其中定义的一个方法。这两个方法都会“抛”出一个新的异常类型:`RainedOut`。当执行到`StormyInning extends``implements Storm`的时候,可以看到`Storm`中的`event()`方法不能改变`Inning`中的`event()`的异常接口。同样地,这种设计是十分合理的;否则的话,当我们操作基础类时,便根本无法知道自己捕获的是否正确的东西。当然,假如`interface`中定义的一个方法不在基础类里,比如`rainHard()`,它产生异常时就没什么问题。
对异常的限制并不适用于构造器。在StormyInning中,我们可看到一个构造器能够“抛”出它希望的任何东西,无论基础类构造器“抛”出什么。然而,由于必须坚持按某种方式调用基础类构造器(在这里,会自动调用默认构造器),所以衍生类构造器必须在自己的异常规范中声明所有基础类构造器异常。
对异常的限制并不适用于构造器。在`StormyInning`中,我们可看到一个构造器能够“抛”出它希望的任何东西,无论基础类构造器“抛”出什么。然而,由于必须坚持按某种方式调用基础类构造器(在这里,会自动调用默认构造器),所以衍生类构造器必须在自己的异常规范中声明所有基础类构造器异常。
StormyInning.walk()不会编译的原因是它“抛”出了一个异常,而Inning.walk()却不会“抛”出。若允许这种情况发生,就可让自己的代码调用Inning.walk(),而且它不必控制任何异常。但在以后替换从Inning衍生的一个类的对象时,异常就会“抛”出,造成代码执行的中断。通过强迫衍生类方法遵守基础类方法的异常规范,对象的替换可保持连贯性。
`StormyInning.walk()`不会编译的原因是它“抛”出了一个异常,而`Inning.walk()`却不会“抛”出。若允许这种情况发生,就可让自己的代码调用`Inning.walk()`,而且它不必控制任何异常。但在以后替换从`Inning`衍生的一个类的对象时,异常就会“抛”出,造成代码执行的中断。通过强迫衍生类方法遵守基础类方法的异常规范,对象的替换可保持连贯性。
覆盖过的event()方法向我们显示出一个方法的衍生类版本可以不产生任何异常——即便基础类版本要产生异常。同样地,这样做是必要的,因为它不会中断那些已假定基础类版本会产生异常的代码。差不多的道理亦适用于atBat(),它会“抛”出PopFoul——从Foul衍生出来的一个异常,而Foul异常是由atBat()的基础类版本产生的。这样一来,假如有人在自己的代码里操作Inning,同时调用了atBat(),就必须捕获Foul异常。由于PopFoul是从Foul衍生的,所以异常控制器(模块)也会捕获PopFoul
覆盖过的`event()`方法向我们显示出一个方法的衍生类版本可以不产生任何异常——即便基础类版本要产生异常。同样地,这样做是必要的,因为它不会中断那些已假定基础类版本会产生异常的代码。差不多的道理亦适用于`atBat()`,它会“抛”出`PopFoul`——从`Foul`衍生出来的一个异常,而`Foul`异常是由`atBat()`的基础类版本产生的。这样一来,假如有人在自己的代码里操作`Inning`,同时调用了`atBat()`,就必须捕获`Foul`异常。由于`PopFoul`是从`Foul`衍生的,所以异常控制器(模块)也会捕获`PopFoul`
最后一个有趣的地方在main()内部。在这个地方,假如我们明确操作一个StormyInning对象,编译器就会强迫我们只捕获特定于那个类的异常。但假如我们向上转换到基础类型,编译器就会强迫我们捕获针对基础类的异常。通过所有这些限制,异常控制代码的“健壮”程度获得了大幅度改善(注释③)。
最后一个有趣的地方在`main()`内部。在这个地方,假如我们明确操作一个`StormyInning`对象,编译器就会强迫我们只捕获特定于那个类的异常。但假如我们向上转换到基础类型,编译器就会强迫我们捕获针对基础类的异常。通过所有这些限制,异常控制代码的“健壮”程度获得了大幅度改善(注释③)。
③:ANSI/ISO C++施加了类似的限制,要求衍生方法异常与基础类方法抛出的异常相同,或者从后者衍生。在这种情况下,C++实际上能够在编译期间检查异常规范。
......
# 9.6 用finally清除
无论一个异常是否在try块中发生,我们经常都想执行一些特定的代码。对一些特定的操作,经常都会遇到这种情况,但在恢复内存时一般都不需要(因为垃圾收集器会自动照料一切)。为达到这个目的,可在所有异常控制器的末尾使用一个finally从句(注释④)。所以完整的异常控制小节象下面这个样子:
无论一个异常是否在`try`块中发生,我们经常都想执行一些特定的代码。对一些特定的操作,经常都会遇到这种情况,但在恢复内存时一般都不需要(因为垃圾收集器会自动照料一切)。为达到这个目的,可在所有异常控制器的末尾使用一个`finally`从句(注释④)。所以完整的异常控制小节象下面这个样子:
```
try {
......@@ -17,9 +17,9 @@ try {
}
```
④:C++异常控制未提供finally从句,因为它依赖构造器来达到这种清除效果。
④:C++异常控制未提供`finally`从句,因为它依赖构造器来达到这种清除效果。
为演示finally从句,请试验下面这个程序:
为演示`finally`从句,请试验下面这个程序:
```
//: FinallyWorks.java
......@@ -45,7 +45,7 @@ public class FinallyWorks {
} ///:~
```
通过该程序,我们亦可知道如何应付Java异常(类似C++的异常)不允许我们恢复至异常产生地方的这一事实。若将自己的try块置入一个循环内,就可建立一个条件,它必须在继续程序之前满足。亦可添加一个static计数器或者另一些设备,允许循环在放弃以前尝试数种不同的方法。这样一来,我们的程序可以变得更加“健壮”。
通过该程序,我们亦可知道如何应付Java异常(类似C++的异常)不允许我们恢复至异常产生地方的这一事实。若将自己的`try`块置入一个循环内,就可建立一个条件,它必须在继续程序之前满足。亦可添加一个`static`计数器或者另一些设备,允许循环在放弃以前尝试数种不同的方法。这样一来,我们的程序可以变得更加“健壮”。
输出如下:
......@@ -56,15 +56,15 @@ No exception
in finally clause
```
无论是否“抛”出一个异常,finally从句都会执行。
无论是否“抛”出一个异常,`finally`从句都会执行。
9.6.1 用finally做什么
9.6.1 用`finally`做什么
在没有“垃圾收集”以及“自动调用析构器”机制的一种语言中(注释⑤),finally显得特别重要,因为程序员可用它担保内存的正确释放——无论在try块内部发生了什么状况。但Java提供了垃圾收集机制,所以内存的释放几乎绝对不会成为问题。另外,它也没有构造器可供调用。既然如此,Java里何时才会用到finally呢?
在没有“垃圾收集”以及“自动调用析构器”机制的一种语言中(注释⑤),`finally`显得特别重要,因为程序员可用它担保内存的正确释放——无论在`try`块内部发生了什么状况。但Java提供了垃圾收集机制,所以内存的释放几乎绝对不会成为问题。另外,它也没有构造器可供调用。既然如此,Java里何时才会用到`finally`呢?
⑤:“析构器”(Destructor)是“构造器”(Constructor)的反义词。它代表一个特殊的函数,一旦某个对象失去用处,通常就会调用它。我们肯定知道在哪里以及何时调用析构器。C++提供了自动的析构器调用机制,但Delphi的Object Pascal版本1及2却不具备这一能力(在这种语言中,析构器的含义与用法都发生了变化)。
除将内存设回原始状态以外,若要设置另一些东西,finally就是必需的。例如,我们有时需要打开一个文件或者建立一个网络连接,或者在屏幕上画一些东西,甚至设置外部世界的一个开关,等等。如下例所示:
除将内存设回原始状态以外,若要设置另一些东西,`finally`就是必需的。例如,我们有时需要打开一个文件或者建立一个网络连接,或者在屏幕上画一些东西,甚至设置外部世界的一个开关,等等。如下例所示:
```
//: OnOffSwitch.java
......@@ -95,7 +95,7 @@ public class OnOffSwitch {
} ///:~
```
这里的目标是保证main()完成时开关处于关闭状态,所以将sw.off()置于try块以及每个异常控制器的末尾。但产生的一个异常有可能不是在这里捕获的,这便会错过sw.off()。然而,利用finally,我们可以将来自try块的关闭代码只置于一个地方:
这里的目标是保证`main()`完成时开关处于关闭状态,所以将`sw.off()`置于`try`块以及每个异常控制器的末尾。但产生的一个异常有可能不是在这里捕获的,这便会错过`sw.off()`。然而,利用`finally`,我们可以将来自`try`块的关闭代码只置于一个地方:
```
//: WithFinally.java
......@@ -125,9 +125,9 @@ public class WithFinally {
} ///:~
```
在这儿,sw.off()已移至一个地方。无论发生什么事情,都肯定会运行它。
在这儿,`sw.off()`已移至一个地方。无论发生什么事情,都肯定会运行它。
即使异常不在当前的catch从句集里捕获,finally都会在异常控制机制转到更高级别搜索一个控制器之前得以执行。如下所示:
即使异常不在当前的`catch`从句集里捕获,`finally`都会在异常控制机制转到更高级别搜索一个控制器之前得以执行。如下所示:
```
//: AlwaysFinally.java
......@@ -169,11 +169,11 @@ Caught Ex in first try block
finally in 1st try block
```
若调用了break和continue语句,finally语句也会得以执行。请注意,与作上标签的break和continue一道,finally排除了Java对goto跳转语句的需求。
若调用了`break``continue`语句,`finally`语句也会得以执行。请注意,与作上标签的`break``continue`一道,`finally`排除了Java对`goto`跳转语句的需求。
9.6.2 缺点:丢失的异常
一般情况下,Java的异常实现方案都显得十分出色。不幸的是,它依然存在一个缺点。尽管异常指出程序里存在一个危机,而且绝不应忽略,但一个异常仍有可能简单地“丢失”。在采用finally从句的一种特殊配置下,便有可能发生这种情况:
一般情况下,Java的异常实现方案都显得十分出色。不幸的是,它依然存在一个缺点。尽管异常指出程序里存在一个危机,而且绝不应忽略,但一个异常仍有可能简单地“丢失”。在采用`finally`从句的一种特殊配置下,便有可能发生这种情况:
```
//: LostMessage.java
......@@ -198,7 +198,7 @@ public class LostMessage {
void dispose() throws HoHumException {
throw new HoHumException();
}
public static void main(String[] args)
public static void main(String[] args)
throws Exception {
LostMessage lm = new LostMessage();
try {
......@@ -218,6 +218,6 @@ A trivial exception
at LostMessage.main(LostMessage.java:29)
```
可以看到,这里不存在VeryImportantException(非常重要的异常)的迹象,它只是简单地被finally从句中的HoHumException代替了。
可以看到,这里不存在`VeryImportantException`(非常重要的异常)的迹象,它只是简单地被`finally`从句中的`HoHumException`代替了。
这是一项相当严重的缺陷,因为它意味着一个异常可能完全丢失。而且就象前例演示的那样,这种丢失显得非常“自然”,很难被人查出蛛丝马迹。而与此相反,C++里如果第二个异常在第一个异常得到控制前产生,就会被当作一个严重的编程错误处理。或许Java以后的版本会纠正这个问题(上述结果是用Java 1.1生成的)。
......@@ -3,9 +3,9 @@
为异常编写代码时,我们经常要解决的一个问题是:“一旦产生异常,会正确地进行清除吗?”大多数时候都会非常安全,但在构造器中却是一个大问题。构造器将对象置于一个安全的起始状态,但它可能执行一些操作——如打开一个文件。除非用户完成对象的使用,并调用一个特殊的清除方法,否则那些操作不会得到正确的清除。若从一个构造器内部“抛”出一个异常,这些清除行为也可能不会正确地发生。所有这些都意味着在编写构造器时,我们必须特别加以留意。
由于前面刚学了finally,所以大家可能认为它是一种合适的方案。但事情并没有这么简单,因为finally每次都会执行清除代码——即使我们在清除方法运行之前不想执行清除代码。因此,假如真的用finally进行清除,必须在构造器正常结束时设置某种形式的标志。而且只要设置了标志,就不要执行finally块内的任何东西。由于这种做法并不完美(需要将一个地方的代码同另一个地方的结合起来),所以除非特别需要,否则一般不要尝试在finally中进行这种形式的清除。
由于前面刚学了`finally`,所以大家可能认为它是一种合适的方案。但事情并没有这么简单,因为`finally`每次都会执行清除代码——即使我们在清除方法运行之前不想执行清除代码。因此,假如真的用`finally`进行清除,必须在构造器正常结束时设置某种形式的标志。而且只要设置了标志,就不要执行`finally`块内的任何东西。由于这种做法并不完美(需要将一个地方的代码同另一个地方的结合起来),所以除非特别需要,否则一般不要尝试在`finally`中进行这种形式的清除。
在下面这个例子里,我们创建了一个名为InputFile的类。它的作用是打开一个文件,然后每次读取它的一行内容(转换为一个字符串)。它利用了由Java标准IO库提供的FileReader以及BufferedReader类(将于第10章讨论)。这两个类都非常简单,大家现在可以毫无困难地掌握它们的基本用法:
在下面这个例子里,我们创建了一个名为`InputFile`的类。它的作用是打开一个文件,然后每次读取它的一行内容(转换为一个字符串)。它利用了由Java标准`IO`库提供的`FileReader`以及`BufferedReader`类(将于第10章讨论)。这两个类都非常简单,大家现在可以毫无困难地掌握它们的基本用法:
```
//: Cleanup.java
......@@ -17,7 +17,7 @@ class InputFile {
private BufferedReader in;
InputFile(String fname) throws Exception {
try {
in =
in =
new BufferedReader(
new FileReader(fname));
// Other code that might throw exceptions
......@@ -63,7 +63,7 @@ class InputFile {
public class Cleanup {
public static void main(String[] args) {
try {
InputFile in =
InputFile in =
new InputFile("Cleanup.java");
String s;
int i = 1;
......@@ -81,15 +81,15 @@ public class Cleanup {
该例使用了Java 1.1 IO类。
用于InputFile的构造器采用了一个String(字符串)参数,它代表我们想打开的那个文件的名字。在一个try块内部,它用该文件名创建了一个FileReader。对FileReader来说,除非转移并用它创建一个能够实际与之“交谈”的BufferedReader,否则便没什么用处。注意InputFile的一个好处就是它同时合并了这两种行动。
用于`InputFile`的构造器采用了一个`String`(字符串)参数,它代表我们想打开的那个文件的名字。在一个`try`块内部,它用该文件名创建了一个`FileReader`。对`FileReader`来说,除非转移并用它创建一个能够实际与之“交谈”的`BufferedReader`,否则便没什么用处。注意`InputFile`的一个好处就是它同时合并了这两种行动。
FileReader构造器不成功,就会产生一个FileNotFoundException(文件未找到异常)。必须单独捕获这个异常——这属于我们不想关闭文件的一种特殊情况,因为文件尚未成功打开。其他任何捕获从句(catch)都必须关闭文件,因为文件已在进入那些捕获从句时打开(当然,如果多个方法都能产生一个FileNotFoundException异常,就需要稍微用一些技巧。此时,我们可将不同的情况分隔到数个try块内)。close()方法会抛出一个尝试过的异常。即使它在另一个catch从句的代码块内,该异常也会得以捕获——对Java编译器来说,那个catch从句不过是另一对花括号而已。执行完本地操作后,异常会被重新“抛”出。这样做是必要的,因为这个构造器的执行已经失败,我们不希望调用方法来假设对象已正确创建以及有效。
`FileReader`构造器不成功,就会产生一个`FileNotFoundException`(文件未找到异常)。必须单独捕获这个异常——这属于我们不想关闭文件的一种特殊情况,因为文件尚未成功打开。其他任何捕获从句(`catch`)都必须关闭文件,因为文件已在进入那些捕获从句时打开(当然,如果多个方法都能产生一个`FileNotFoundException`异常,就需要稍微用一些技巧。此时,我们可将不同的情况分隔到数个`try`块内)。`close()`方法会抛出一个尝试过的异常。即使它在另一个`catch`从句的代码块内,该异常也会得以捕获——对Java编译器来说,那个`catch`从句不过是另一对花括号而已。执行完本地操作后,异常会被重新“抛”出。这样做是必要的,因为这个构造器的执行已经失败,我们不希望调用方法来假设对象已正确创建以及有效。
在这个例子中,没有采用前述的标志技术,finally从句显然不是关闭文件的正确地方,因为这可能在每次构造器结束的时候关闭它。由于我们希望文件在InputFile对象处于活动状态时一直保持打开状态,所以这样做并不恰当。
在这个例子中,没有采用前述的标志技术,`finally`从句显然不是关闭文件的正确地方,因为这可能在每次构造器结束的时候关闭它。由于我们希望文件在`InputFile`对象处于活动状态时一直保持打开状态,所以这样做并不恰当。
getLine()方法会返回一个字符串,其中包含了文件中下一行的内容。它调用了readLine(),后者可能产生一个异常,但那个异常会被捕获,使getLine()不会再产生任何异常。对异常来说,一项特别的设计问题是决定在这一级完全控制一个异常,还是进行部分控制,并传递相同(或不同)的异常,或者只是简单地传递它。在适当的时候,简单地传递可极大简化我们的编码工作。
`getLine()`方法会返回一个字符串,其中包含了文件中下一行的内容。它调用了`readLine()`,后者可能产生一个异常,但那个异常会被捕获,使`getLine()`不会再产生任何异常。对异常来说,一项特别的设计问题是决定在这一级完全控制一个异常,还是进行部分控制,并传递相同(或不同)的异常,或者只是简单地传递它。在适当的时候,简单地传递可极大简化我们的编码工作。
getLine()方法会变成:
`getLine()`方法会变成:
```
String getLine() throws IOException {
......@@ -97,12 +97,12 @@ return in.readLine();
}
```
但是当然,调用者现在需要对可能产生的任何IOException进行控制。
但是当然,调用者现在需要对可能产生的任何`IOException`进行控制。
用户使用完毕InputFile对象后,必须调用cleanup()方法,以便释放由BufferedReader以及/或者FileReader占用的系统资源(如文件引用)——注释⑥。除非InputFile对象使用完毕,而且到了需要弃之不用的时候,否则不应进行清除。大家可能想把这样的机制置入一个finalize()方法内,但正如第4章指出的那样,并非总能保证finalize()获得正确的调用(即便确定它会调用,也不知道何时开始)。这属于Java的一项缺陷——除内存清除之外的所有清除都不会自动进行,所以必须知会客户程序员,告诉他们有责任用finalize()保证清除工作的正确进行。
用户使用完毕`InputFile`对象后,必须调用`cleanup()`方法,以便释放由`BufferedReader`以及/或者`FileReader`占用的系统资源(如文件引用)——注释⑥。除非`InputFile`对象使用完毕,而且到了需要弃之不用的时候,否则不应进行清除。大家可能想把这样的机制置入一个`finalize()`方法内,但正如第4章指出的那样,并非总能保证`finalize()`获得正确的调用(即便确定它会调用,也不知道何时开始)。这属于Java的一项缺陷——除内存清除之外的所有清除都不会自动进行,所以必须知会客户程序员,告诉他们有责任用`finalize()`保证清除工作的正确进行。
⑥:在C++里,“析构器”可帮我们控制这一局面。
Cleanup.java中,我们创建了一个InputFile,用它打开用于创建程序的相同的源文件。同时一次读取该文件的一行内容,而且添加相应的行号。所有异常都会在main()中被捕获——尽管我们可选择更大的可靠性。
`Cleanup.java`中,我们创建了一个`InputFile`,用它打开用于创建程序的相同的源文件。同时一次读取该文件的一行内容,而且添加相应的行号。所有异常都会在`main()`中被捕获——尽管我们可选择更大的可靠性。
这个示例也向大家展示了为何在本书的这个地方引入异常的概念。异常与Java的编程具有很高的集成度,这主要是由于编译器会强制它们。只有知道了如何操作那些异常,才可更进一步地掌握编译器的知识。
......@@ -24,7 +24,7 @@ public class Human {
} ///:~
```
Sneeze异常会被相符的第一个catch从句捕获。当然,这只是第一个。然而,假如我们删除第一个catch从句:
`Sneeze`异常会被相符的第一个`catch`从句捕获。当然,这只是第一个。然而,假如我们删除第一个`catch`从句:
```
try {
......@@ -34,7 +34,7 @@ Sneeze异常会被相符的第一个catch从句捕获。当然,这只是第一
}
```
那么剩下的catch从句依然能够工作,因为它捕获的是Sneeze的基础类。换言之,catch(Annoyance e)能捕获一个Annoyance以及从它衍生的任何类。这一点非常重要,因为一旦我们决定为一个方法添加更多的异常,而且它们都是从相同的基础类继承的,那么客户程序员的代码就不需要更改。至少能够假定它们捕获的是基础类。
那么剩下的`catch`从句依然能够工作,因为它捕获的是`Sneeze`的基础类。换言之,`catch(Annoyance e)`能捕获一个`Annoyance`以及从它衍生的任何类。这一点非常重要,因为一旦我们决定为一个方法添加更多的异常,而且它们都是从相同的基础类继承的,那么客户程序员的代码就不需要更改。至少能够假定它们捕获的是基础类。
若将基础类捕获从句置于第一位,试图“屏蔽”衍生类异常,就象下面这样:
......@@ -48,7 +48,7 @@ Sneeze异常会被相符的第一个catch从句捕获。当然,这只是第一
}
```
则编译器会产生一条出错消息,因为它发现永远不可能抵达Sneeze捕获从句。
则编译器会产生一条出错消息,因为它发现永远不可能抵达`Sneeze`捕获从句。
9.8.1 异常准则
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册