第9章 异常差错控制Word文档格式.docx
《第9章 异常差错控制Word文档格式.docx》由会员分享,可在线阅读,更多相关《第9章 异常差错控制Word文档格式.docx(22页珍藏版)》请在冰点文库上搜索。
![第9章 异常差错控制Word文档格式.docx](https://file1.bingdoc.com/fileroot1/2023-4/29/5bbb2de2-faa1-4817-afa8-58bbd6b2d38a/5bbb2de2-faa1-4817-afa8-58bbd6b2d38a1.gif)
我们只需要在一个地方处理问题:
“违例控制模块”或者“违例控制器”。
这样可有效减少代码量,并将那些用于描述具体操作的代码与专门纠正错误的代码分隔开。
一般情况下,用于读取、写入以及调试的代码会变得更富有条理。
由于违例控制是由Java编译器强行实施的,所以毋需深入学习违例控制,便可正确使用本书编写的大量例子。
本章向大家介绍了用于正确控制违例所需的代码,以及在某个方法遇到麻烦的时候,该如何生成自己的违例。
9.1基本违例
“违例条件”表示在出现什么问题的时候应中止方法或作用域的继续。
为了将违例条件与普通问题区分开,违例条件是非常重要的一个因素。
在普通问题的情况下,我们在当地已拥有足够的信息,可在某种程度上解决碰到的问题。
而在违例条件的情况下,却无法继续下去,因为当地没有提供解决问题所需的足够多的信息。
此时,我们能做的唯一事情就是跳出当地环境,将那个问题委托给一个更高级的负责人。
这便是出现违例时出现的情况。
一个简单的例子是“除法”。
如可能被零除,就有必要进行检查,确保程序不会冒进,并在那种情况下执行除法。
但具体通过什么知道分母是零呢?
在那个特定的方法里,在我们试图解决的那个问题的环境中,我们或许知道该如何对待一个零分母。
但假如它是一个没有预料到的值,就不能对其进行处理,所以必须产生一个违例,而非不顾一切地继续执行下去。
产生一个违例时,会发生几件事情。
首先,按照与创建Java对象一样的方法创建违例对象:
在内存“堆”里,使用new来创建。
随后,停止当前执行路径(记住不可沿这条路径继续下去),然后从当前的环境中释放出违例对象的句柄。
此时,违例控制机制会接管一切,并开始查找一个恰当的地方,用于继续程序的执行。
这个恰当的地方便是“违例控制器”,它的职责是从问题中恢复,使程序要么尝试另一条执行路径,要么简单地继续。
作为产生违例的一个简单示例,大家可思考一个名为t的对象句柄。
有些时候,程序可能传递一个尚未初始化的句柄。
所以在用那个对象句柄调用一个方法之前,最好进行一番检查。
可将与错误有关的信息发送到一个更大的场景中,方法是创建一个特殊的对象,用它代表我们的信息,并将其“掷”(Throw)出我们当前的场景之外。
这就叫作“产生一个违例”或者“掷出一个违例”。
下面是它的大概形式:
if(t==null)
thrownewNullPointerException();
这样便“掷”出了一个违例。
在当前场景中,它使我们能放弃进一步解决该问题的企图。
该问题会被转移到其他更恰当的地方解决。
准确地说,那个地方不久就会显露出来。
9.1.1违例自变量
和Java的其他任何对象一样,需要用new在内存堆里创建违例,并需调用一个构建器。
在所有标准违例中,存在着两个构建器:
第一个是默认构建器,第二个则需使用一个字串自变量,使我们能在违例里置入相关信息:
thrownewNullPointerException("
t=null"
);
稍后,字串可用各种方法提取出来,就象稍后会展示的那样。
在这儿,关键字throw会象变戏法一样做出一系列不可思议的事情。
它首先执行new表达式,创建一个不在程序常规执行范围之内的对象。
而且理所当然,会为那个对象调用构建器。
随后,对象实际会从方法中返回——尽管对象的类型通常并不是方法设计为返回的类型。
为深入理解违例控制,可将其想象成另一种返回机制——但是不要在这个问题上深究,否则会遇到麻烦。
通过“掷”出一个违例,亦可从原来的作用域中退出。
但是会先返回一个值,再退出方法或作用域。
但是,与普通方法返回的相似性到此便全部结束了,因为我们返回的地方与从普通方法调用中返回的地方是迥然有异的(我们结束于一个恰当的违例控制器,它距离违例“掷”出的地方可能相当遥远——在调用堆栈中要低上许多级)。
此外,我们可根据需要掷出任何类型的“可掷”对象。
典型情况下,我们要为每种不同类型的错误“掷”出一类不同的违例。
我们的思路是在违例对象以及挑选的违例对象类型中保存信息,所以在更大场景中的某个人可知道如何对待我们的违例(通常,唯一的信息是违例对象的类型,而违例对象中保存的没什么意义)。
9.2违例的捕获
若某个方法产生一个违例,必须保证该违例能被捕获,并获得正确对待。
对于Java的违例控制机制,它的一个好处就是允许我们在一个地方将精力集中在要解决的问题上,然后在另一个地方对待来自那个代码内部的错误。
为理解违例是如何捕获的,首先必须掌握“警戒区”的概念。
它代表一个特殊的代码区域,有可能产生违例,并在后面跟随用于控制那些违例的代码。
9.2.1try块
若位于一个方法内部,并“掷”出一个违例(或在这个方法内部调用的另一个方法产生了违例),那个方法就会在违例产生过程中退出。
若不想一个throw离开方法,可在那个方法内部设置一个特殊的代码块,用它捕获违例。
这就叫作“try块”,因为要在这个地方“尝试”各种方法调用。
try块属于一种普通的作用域,用一个try关键字开头:
try{
//可能产生违例的代码
}
若用一种不支持违例控制的编程语言全面检查错误,必须用设置和错误检测代码将每个方法都包围起来——即便多次调用相同的方法。
而在使用了违例控制技术后,可将所有东西都置入一个try块内,在同一地点捕获所有违例。
这样便可极大简化我们的代码,并使其更易辨读,因为代码本身要达到的目标再也不会与繁复的错误检查混淆。
9.2.2违例控制器
当然,生成的违例必须在某个地方中止。
这个“地方”便是违例控制器或者违例控制模块。
而且针对想捕获的每种违例类型,都必须有一个相应的违例控制器。
违例控制器紧接在try块后面,且用catch(捕获)关键字标记。
如下所示:
407-408页程序
每个catch从句——即违例控制器——都类似一个小型方法,它需要采用一个(而且只有一个)特定类型的自变量。
可在控制器内部使用标识符(id1,id2等等),就象一个普通的方法自变量那样。
我们有时也根本不使用标识符,因为违例类型已提供了足够的信息,可有效处理违例。
但即使不用,标识符也必须就位。
控制器必须“紧接”在try块后面。
若“掷”出一个违例,违例控制机制就会搜寻自变量与违例类型相符的第一个控制器。
随后,它会进入那个catch从句,并认为违例已得到控制(一旦catch从句结束,对控制器的搜索也会停止)。
只有相符的catch从句才会得到执行;
它与switch语句不同,后者在每个case后都需要一个break命令,防止误执行其他语句。
在try块内部,请注意大量不同的方法调用可能生成相同的违例,但只需要一个控制器。
1.中断与恢复
在违例控制理论中,共存在两种基本方法。
在“中断”方法中(Java和C++提供了对这种方法的支持),我们假定错误非常关键,没有办法返回违例发生的地方。
无论谁只要“掷”出一个违例,就表明没有办法补救错误,而且也不希望再回来。
另一种方法叫作“恢复”。
它意味着违例控制器有责任来纠正当前的状况,然后取得出错的方法,假定下一次会成功执行。
若使用恢复,意味着在违例得到控制以后仍然想继续执行。
在这种情况下,我们的违例更象一个方法调用——我们用它在Java中设置各种各样特殊的环境,产生类似于“恢复”的行为(换言之,此时不是“掷”出一个违例,而是调用一个用于解决问题的方法)。
另外,也可以将自己的try块置入一个while循环里,用它不断进入try块,直到结果满意时为止。
从历史的角度看,若程序员使用的操作系统支持可恢复的违例控制,最终都会用到类似于中断的代码,并跳过恢复进程。
所以尽管“恢复”表面上十分不错,但在实际应用中却显得困难重重。
其中决定性的原因可能是:
我们的控制模块必须随时留意是否产生了违例,以及是否包含了由产生位置专用的代码。
这便使代码很难编写和维护——大型系统尤其如此,因为违例可能在多个位置产生。
9.2.3违例规范
在Java中,对那些要调用方法的客户程序员,我们要通知他们可能从自己的方法里“掷”出违例。
这是一种有礼貌的做法,只有它才能使客户程序员准确地知道要编写什么代码来捕获所有潜在的违例。
当然,若你同时提供了源码,客户程序员甚至能全盘检查代码,找出相应的throw语句。
但尽管如此,通常并不随同源码提供库。
为解决这个问题,Java提供了一种特殊的语法格式(并强迫我们采用),以便礼貌地告诉客户程序员该方法会“掷”出什么违例,令对方方便地加以控制。
这便是我们在这里要讲述的“违例规范”,它属于方法声明的一部分,位于自变量(参数)列表的后面。
违例规范采用了一个额外的关键字:
throws;
后面跟随全部潜在的违例类型。
因此,我们的方法定义看起来应象下面这个样子:
voidf()throwstooBig,tooSmall,divZero{//...
若使用下述代码:
voidf()[//...
它意味着不会从方法里“掷”出违例(除类型为RuntimeException的违例以外,它可能从任何地方掷出——稍后还会详细讲述)。
但不能完全依赖违例规范——假若方法造成了一个违例,但没有对其进行控制,编译器会侦测到这个情况,并告诉我们必须控制违例,或者指出应该从方法里“掷”出一个违例规范。
通过坚持从顶部到底部排列违例规范,Java可在编译期保证违例的正确性(注释②)。
②:
这是在C++违例控制基础上一个显著的进步,后者除非到运行期,否则不会捕获不符合违例规范的错误。
这使得C++的违例控制机制显得用处不大。
我们在这个地方可采取欺骗手段:
要求“掷”出一个并没有发生的违例。
编译器能理解我们的要求,并强迫使用这个方法的用户当作真的产生了那个违例处理。
在实际应用中,可将其作为那个违例的一个“占位符”使用。
这样一来,以后可以方便地产生实际的违例,毋需修改现有的代码。
9.2.4捕获所有违例
我们可创建一个控制器,令其捕获所有类型的违例。
具体的做法是捕获基础类违例类型Exception(也存在其他类型的基础违例,但Exception是适用于几乎所有编程活动的基础)。
catch(Exceptione){
System.out.println("
caughtanexception"
这段代码能捕获任何违例,所以在实际使用时最好将其置于控制器列表的末尾,防止跟随在后面的任何特殊违例控制器失效。
对于程序员常用的所有违例类来说,由于Exception类是它们的基础,所以我们不会获得关于违例太多的信息,但可调用来自它的基础类Throwable的方法:
StringgetMessage()
获得详细的消息。
StringtoString()
返回对Throwable的一段简要说明,其中包括详细的消息(如果有的话)。
voidprintStackTrace()
voidprintStackTrace(PrintStream)
打印出Throwable和Throwable的调用堆栈路径。
调用堆栈显示出将我们带到违例发生地点的方法调用的顺序。
第一个版本会打印出标准错误,第二个则打印出我们的选择流程。
若在Windows下工作,就不能重定向标准错误。
因此,我们一般愿意使用第二个版本,并将结果送给System.out;
这样一来,输出就可重定向到我们希望的任何路径。
除此以外,我们还可从Throwable的基础类Object(所有对象的基础类型)获得另外一些方法。
对于违例控制来说,其中一个可能有用的是getClass(),它的作用是返回一个对象,用它代表这个对象的类。
我们可依次用getName()或toString()查询这个Class类的名字。
亦可对Class对象进行一些复杂的操作,尽管那些操作在违例控制中是不必要的。
本章稍后还会详细讲述Class对象。
下面是一个特殊的例子,它展示了Exception方法的使用(若执行该程序遇到困难,请参考第3章3.1.2小节“赋值”):
411页中程序
该程序输出如下:
411页下程序
可以看到,该方法连续提供了大量信息——每类信息都是前一类信息的一个子集。
9.2.5重新“掷”出违例
在某些情况下,我们想重新掷出刚才产生过的违例,特别是在用Exception捕获所有可能的违例时。
由于我们已拥有当前违例的句柄,所以只需简单地重新掷出那个句柄即可。
下面是一个例子:
一个违例已经产生"
throwe;
重新“掷”出一个违例导致违例进入更高一级环境的违例控制器中。
用于同一个try块的任何更进一步的catch从句仍然会被忽略。
此外,与违例对象有关的所有东西都会得到保留,所以用于捕获特定违例类型的更高一级的控制器可以从那个对象里提取出所有信息。
若只是简单地重新掷出当前违例,我们打印出来的、与printStackTrace()内的那个违例有关的信息会与违例的起源地对应,而不是与重新掷出它的地点对应。
若想安装新的堆栈跟踪信息,可调用fillInStackTrace(),它会返回一个特殊的违例对象。
这个违例的创建过程如下:
将当前堆栈的信息填充到原来的违例对象里。
下面列出它的形式:
412-413页程序
其中最重要的行号在注释内标记出来。
注意第17行没有设为注释行。
它的输出结果如下:
413页中程序
因此,违例堆栈路径无论如何都会记住它的真正起点,无论自己被重复“掷”了好几次。
若将第17行标注(变成注释行),而撤消对第18行的标注,就会换用fillInStackTrace(),结果如下:
413页下程序
由于使用的是fillInStackTrace(),第18行成为违例的新起点。
针对g()和main(),Throwable类必须在违例规格中出现,因为fillInStackTrace()会生成一个Throwable对象的句柄。
由于Throwable是Exception的一个基础类,所以有可能获得一个能够“掷”出的对象(具有Throwable属性),但却并非一个Exception(违例)。
因此,在main()中用于Exception的句柄可能丢失自己的目标。
为保证所有东西均井然有序,编译器强制Throwable使用一个违例规范。
举个例子来说,下述程序的违例便不会在main()中被捕获到:
414页上程序
也有可能从一个已经捕获的违例重新“掷”出一个不同的违例。
但假如这样做,会得到与使用fillInStackTrace()类似的效果:
与违例起源地有关的信息会全部丢失,我们留下的是与新的throw有关的信息。
414-415页程序
输出如下:
415页程序
最后一个违例只知道自己来自main(),而非来自f()。
注意Throwable在任何违例规范中都不是必需的。
永远不必关心如何清除前一个违例,或者与之有关的其他任何违例。
它们都属于用new创建的、以内存堆为基础的对象,所以垃圾收集器会自动将其清除。
9.3标准Java违例
Java包含了一个名为Throwable的类,它对可以作为违例“掷”出的所有东西进行了描述。
Throwable对象有两种常规类型(亦即“从Throwable继承”)。
其中,Error代表编译期和系统错误,我们一般不必特意捕获它们(除在特殊情况以外)。
Exception是可以从任何标准Java库的类方法中“掷”出的基本类型。
此外,它们亦可从我们自己的方法以及运行期偶发事件中“掷”出。
为获得违例的一个综合概念,最好的方法是阅读由提供的联机Java文档(当然,首先下载它们更好)。
为了对各种违例有一个大概的印象,这个工作是相当有价值的。
但大家不久就会发现,除名字外,一个违例和下一个违例之间并不存在任何特殊的地方。
此外,Java提供的违例数量正在日益增多;
从本质上说,把它们印到一本书里是没有意义的。
大家从其他地方获得的任何新库可能也提供了它们自己的违例。
我们最需要掌握的是基本概念,以及用这些违例能够做什么。
java.lang.Exception
这是程序能捕获的基本违例。
其他违例都是从它衍生出去的。
这里要注意的是违例的名字代表发生的问题,而且违例名通常都是精心挑选的,可以很清楚地说明到底发生了什么事情。
违例并不全是在java.lang中定义的;
有些是为了提供对其他库的支持,如util,net以及io等——我们可以从它们的完整类名中看出这一点,或者观察它们从什么继承。
例如,所有IO违例都是从java.io.IOException继承的。
9.3.1RuntimeException的特殊情况
本章的第一个例子是:
看起来似乎在传递进入一个方法的每个句柄中都必须检查null(因为不知道调用者是否已传递了一个有效的句柄),这无疑是相当可怕的。
但幸运的是,我们根本不必这样做——它属于Java进行的标准运行期检查的一部分。
若对一个空句柄发出了调用,Java会自动产生一个NullPointerException违例。
所以上述代码在任何情况下都是多余的。
这个类别里含有一系列违例类型。
它们全部由Java自动生成,毋需我们亲自动手把它们包含到自己的违例规范里。
最方便的是,通过将它们置入单独一个名为RuntimeException的基础类下面,它们全部组合到一起。
这是一个很好的继承例子:
它建立了一系列具有某种共通性的类型,都具有某些共通的特征与行为。
此外,我们没必要专门写一个违例规范,指出一个方法可能会“掷”出一个RuntimeException,因为已经假定可能出现那种情况。
由于它们用于指出编程中的错误,所以几乎永远不必专门捕获一个“运行期违例”——RuntimeException——它在默认情况下会自动得到处理。
若必须检查RuntimeException,我们的代码就会变得相当繁复。
在我们自己的包里,可选择“掷”出一部分RuntimeException。
如果不捕获这些违例,又会出现什么情况呢?
由于编译器并不强制违例规范捕获它们,所以假如不捕获的话,一个RuntimeException可能过滤掉我们到达main()方法的所有途径。
为体会此时发生的事情,请试试下面这个例子:
417页上程序
大家已经看到,一个RuntimeException(或者从它继承的任何东西)属于一种特殊情况,因为编译器不要求为这些类型指定违例规范。
java.lang.RuntimeException:
Fromf()
atNeverCaught.f(NeverCaught.java:
9)
atNeverCaught.g(NeverCaught.java:
12)
atNeverCaught.main(NeverCaught.java:
15)
所以答案就是:
假若一个RuntimeException获得到达main()的所有途径,同时不被捕获,那么当程序退出时,会为那个违例调用printStackTrace()。
注意也许能在自己的代码中仅忽略RuntimeException,因为编译器已正确实行了其他所有控制。
因为RuntimeException在此时代表一个编程错误:
(1)一个我们不能捕获的错误(例如,由客户程序员接收传递给自己方法的一个空句柄)。
(2)作为一名程序员,一个应在自己的代码中检查的错误(如ArrayIndexOutOfBoundException,此时应注意数组的大小)。
可以看出,最好的做法是在这种情况下违例,因为它们有助于程序的调试。
另外一个有趣的地方是,我们不可将Java违例划分为单一用途的工具。
的确,它们设计用于控制那些讨厌的运行期错误——由代码控制范围之外的其他力量产生。
但是,它也特别有助于调试某些特殊类型的编程错误,那些是编译器侦测不到的。
9.4创建自己的违例
并不一定非要使用Java违例。
这一点必须掌握,因为经常都需要创建自己的违例,以便指出自己的库可能生成的一个特殊错误——但创建Java分级结构的时候,这个错误是无法预知的。
为创建自己的违例类,必须从一个现有的违例类型继承——最好在含义上与新违例近似。
继承一个违例相当简单:
418-419页程序
继承在创建新类时发生:
419页上程序
这里的关键是“extendsException”,它的意思是:
除包括一个Exception的全部含义以外,还有更多的含义。
增加的代码数量非常少——实际只添加了两个构建器,对MyException的创建方式进行了定义。
请记住,假如我们不明确调用一个基础类构建器,编译器会自动调用基础类默认构建器。
在第二个构建器中,通过使用super关键字,明确调用了带有一个String参数的基础类构建器。
该程序输出结果如下:
419页下程序
可以看到,在从f()“掷”出的MyException违例中,缺乏详细的消息。
创建自己的违例时,还可以采取更多的操作。
我们可添加额外的构建器及成员:
419-421页程序
此时添加了一个数据成员i;
同时添加了一个特殊的方法,用它读取那个值;
也添加了一个额外的构建器,用它设置那个值。
输出结果如下:
421页上程序
由于违例不过是另一种形式的对象,所以可以继续这个进程,进一步增强违例类的能力。
但要注意,对使用自己这个包的客户程序员来说,他们可能错过所有这些增强。
因为他们可能只是简单地寻找准备生成