异常与错误处理.docx
- 文档编号:6331651
- 上传时间:2023-05-09
- 格式:DOCX
- 页数:73
- 大小:73.42KB
异常与错误处理.docx
《异常与错误处理.docx》由会员分享,可在线阅读,更多相关《异常与错误处理.docx(73页珍藏版)》请在冰点文库上搜索。
异常与错误处理
中文
异常与错误处理
Java的基本理念是"结构不佳的代码将不能运行"。
发现错误的理想时机是在编译阶段,也就是在你试图运行程序之前。
然而,编译期间并不能找出所有的错误,余下的问题必须在运行期间得到解决。
这就需要错误源能通过某种方式,把适当的信息传递给某个接收者,后者将知道如何正确处理这个问题。
C以及其它早期语言常常具有多种错误处理模式,这些模式往往建立在约定俗成的基础之上,而并不属于语言的一部分。
通常:
你会返回某个特殊值或者设置某个标志,并且假定接收者将对这个返回值或标志进行检查,以判定是否发生了错误。
然而,随着时间的推移,人们发现,高傲的程序员们在使用程序库的时候更倾向于认为:
"对,错误也许会发生,但那是别人造成的,不关我的事"。
所以,程序员不去检查错误条件,也就不足为奇了(何况对某些错误条件的检查确实显得很无聊)。
如果你在每次调用方法的时候都彻底地进行错误检查,代码很可能会变得难以阅读。
正是由于程序员还能用这些方式拼凑系统,所以他们拒绝承认这样一个事实:
对于构造大型﹑健壮﹑可维护的程序而言,这种错误处理模式已经成为了主要障碍。
解决的办法是,用强制规定的形式来消除错误处理过程中随心所欲的因素。
这种作法由来已久,对"异常处理"(exceptionhandling)的实现可以追溯到六十年代的操作系统,甚至于BASIC语言中的"onerrorgoto"语句。
C++的异常处理机制基于Ada,Java中的异常处理则建立在C++的基础之上(尽管看上去更像ObjectPascal)。
"异常"(exception)这个词有"我对此感到意外"的意思。
问题出现了,你也许不清楚该如何处理,但你的确知道不应该置之不理;你要停下来,看看是不是有别人或是在别的地方,能够处理这个问题。
只是你在当前的环境(currentcontext)中没有足够的信息来解决这个问题,所以你就把这个问题提交到一个更高级别的环境中,这里将有人作出正确的决定(有点像军队里的指挥系统)。
使用异常所带来的另一个相当明显的好处是,它能使错误处理代码变得更有条理。
与原先"对于同一个错误,要在多个地方进行检查和处理"相比,你不必在方法调用处进行检查(因为异常机制将保证捕获这个错误)。
并且,你只需在一个地方处理错误,既所谓的"异常处理程序"(exceptionhandler)。
这种方式不仅节省代码,而且把"描述做什么事"的代码和"出了问题怎么办"的代码相分离。
总之,与以前的错误处理方法相比,异常机制使代码的阅读﹑编写和调试工作更加井井有条。
因为异常处理是Java中唯一正式的错误报告机制,并且通过编译器强制执行,所以不学习异常处理的话,也就只能对付前面学习过的那些例子了。
本章将向你介绍如何编写正确的异常处理程序,以及当你的方法出问题的时候,如何产生自定义的异常。
1比如,C程序员不妨去检查一下prinft()的返回值。
基本异常
"异常情形"(exceptionalcondition)是指引发阻止当前方法或作用域继续执行的问题。
把异常情形与普通问题相区分很重要,这里的普通问题是指,你在当前环境下能得到足够的信息,总能处理这个错误。
而对于异常情形,你就不能继续下去了,因为你在当前环境下无法获得必要的信息来解决问题。
你所能做的就是从当前的环境中跳出,并且把问题提交给上一级别的环境。
这就是抛出异常时所发生的事情。
除法就是个简单的例子。
除数有可能为0,所以先进行检查很有必要。
但除数为0代表的究竟是什么意思呢?
你通过当前正在解决的问题的环境,或许能知道该如何处理除数为0的情况。
但如果这是一个意料之外的值,你也不清楚该如何处理,那就要抛出异常,而不是顺着原来的路径继续执行下去。
当你抛出异常后,有几件事会随之发生。
首先,同Java中其它对象的创建一样,将使用new在堆上创建异常对象。
然后,当前的执行路径(你不能继续下去了)被终止,并且从当前环境中弹出异常对象的引用。
此时,异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。
这个恰当的地方就是"异常处理程序"(exceptionhandler),它的任务是将程序从错误状态中恢复:
以使程序能要么换一种方式运行,要么继续运行下去。
举一个抛出异常的简单例子。
对于对象引用t,传给你的时候可能尚未被初始化。
所以在使用这个引用调用其方法之前,你会先对引用进行检查。
你可以创建一个代表错误信息的对象,并且将它从当前环境中"抛出",这样就把错误信息传播到了"更大"的环境中。
这被称为"抛出一个异常"(throwinganexception),看起来像这样:
if(t==null)
thrownewNullPointerException();
这就抛出了异常,于是你在当前的环境下就不必为这个问题而操心了,它将在别的地方得到处理。
具体是哪个"地方"后面很快就会介绍。
异常形式参数与Java中的其它对象一样,你总是用new在堆上创建异常对象,这也伴随着存储空间的分配和构造器的调用。
所有标准异常类都有两个构造器:
一个是缺省构造器;另一个是接受字符串作为参数,用来把相关信息放入异常对象的构造器:
thrownewNullPointerException("t=null");你将看到,有多种不同的方法可以把这个字符串的内容提取出来。
关键字throw将触发许多十分奇妙的事情。
通常,你首先使用new来创建对象,用以表示错误情况,此对象的引用将传给throw。
尽管返回的异常对象其类型通常与方法设计的返回类型不同,但从效果上看,它就像是从方法"返回"的。
可以简单地把异常处理看成是一种能返回不同类型的机制,当然你过分强调这种类比的话,就会有麻烦了。
你也能用抛出异常的方式从当前的作用域退出。
一旦返回了一个值,就会退出方法或作用域。
抛出异常与方法正常返回值的相似之处到此为止。
因为异常返回的"地点"与普通方法调用返回的"地点"完全不同。
(异常将在一个恰当的异常处理程序中得到解决,它的位置可能离异常被抛出的地方很远,也可能会跨越方法调用栈的许多层次。
)此外,你能抛出任意类型的Throwable(它是异常类型的根类)对象。
通常,对于不同类型的错误,你要抛出相应的异常。
错误信息可以保存在异常对象内部或者用异常类型的名称来暗示。
上一层的环境通过这些信息得以决定如何处理你的异常。
(通常,异常类型的名称就是唯一的信息,而异常对象本身则不包含任何有意义的内容。
)捕获异常如果方法要抛出异常,它必须假定异常将被"捕获"并得到处理。
异常处理的好处之一就是,使你得以先在一个地方专注于正在解决的问题,然后在别的地方处理这些代码中可能发生的错误。
要明白异常是如何被捕获的,你必须首先理解监控区域(guardedregion)的概念。
它是一段可能产生异常的代码,并且后面跟着针对这些异常的处理程序。
Try块
如果你在方法内部抛出了异常(或者在方法内部调用的其它方法抛出了异常),这个方法将在抛出异常的过程中结束。
要是你不希望方法就此结束,你可以在方法内设置一个特殊的块来捕获异常。
因为你在这个块里"尝试"调用了一些(可能产生异常的)方法,所以称为try区块。
它是跟在try关键字之后的普通程序块:
try{//Codethatmightgenerateexceptions
}对于不支持异常处理的程序语言,要想仔细检查错误,你就得在每个方法调用的前后加上设置和错误检查的代码,甚至你每次调用同一方法时也得这么做。
有了异常处理机制,你可以把所有动作都放在try区块里,然后只需在一个地方就可以捕获所有异常。
这意味着代码将更容易被编写和阅读,因为完成任务的代码没有与错误检查的代码混在一起。
异常处理程序(Exceptionhandler)
当然,抛出的异常必须在某处得到处理。
这个"地点"就是"异常处理程序"(exceptionhandler),针对每个要捕获的异常,你得准备相应的处理程序。
异常处理程序紧跟在try区块之后,以关键字catch表示:
try{
//Codethatmightgenerateexceptions
}catch(Type1id1){
//HandleexceptionsofType1
}catch(Type2id2){
//HandleexceptionsofType2
}catch(Type3id3){
//HandleexceptionsofType3
}//etc...每个catch子句(异常处理程序)看起来就像是仅仅接受一个特定参数的方法。
可以在处理程序的内部使用标识符(id1,id2等等),这与方法参数的使用很相似。
有时你可能用不到标识符,因为异常的类型已经给了你足够的信息来对异常进行处理,但标识符并不可以省略。
异常处理程序必须紧跟在try块之后。
当异常被抛出时,异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序。
然后进入catch子句执行,此时认为异常得到了处理。
一旦catch子句结束,则处理程序的查找过程结束。
注意,只有匹配的catch子句才能得到执行;这与switch语句不同,switch语句需要你在每一个case后面跟一个break,以避免执行后续的case子句。
注意在try块的内部,不同的方法调用可能会产生类型相同的异常,你只需要提供一个针对此类型的异常处理程序。
终止与恢复(Terminationvs.Resumption)异常处理理论上有两种基本模型。
一种称为"终止模型"(它是Java和C++所支持的模型)。
在这种模型中,将假设错误非常关键,以至于程序无法返回到异常发生的地方继续执行。
一旦异常被抛出,就表明错误已无法挽回,也不能回来继续执行。
另一种称为"恢复模型"。
意思是异常处理程序的工作是修正错误,然后重新尝试调用出问题的方法,并认为第二次能成功。
对于恢复模型,你希望异常被处理之后,能继续执行程序。
在这种情况下,抛出异常更像是对方法的调用----你可以在Java里用这种方法进行配置,以得到类似"恢复"的行为。
(换句话说,不是抛出异常,而是调用方法来修正错误。
)或者,把try块放在while循环里,这样就不断地进入try块,直到得到满意的结果。
长久以来,尽管程序员们使用的操作系统支持恢复模型的异常处理,但他们最终还是转向使用类似终结模型的代码,并且忽略恢复行为。
所以虽然恢复模型开始显得很吸引人,但不是很实用。
其中的主要原因可能是它所导致的"耦合":
你的处理程序必须关注异常抛出的地点,这势必要包含依赖于抛出位置的非一般性代码。
这增加了代码编写和维护的困难,对于异常可能会从许多地方抛出的大型程序来说,更是如此。
创建自定义异常你不必拘泥于Java中已有的异常类型。
JDK提供的异常体系不能预见你想报告的所有错误,所以你可以自己定义异常类来表示程序中可能遇到的特定问题。
要自己定义异常类,你必须从已有的异常类继承,最好是选择意思相近的异常类继承(不过这样的异常并不容易找)。
建立新的异常类型最简单的方法就是让编译器为你产生缺省构造器,所以这几乎不用写多少代码:
//:
c09:
SimpleExceptionDemo.java
//Inheritingyourownexceptions.
privatestaticTestmonitor=newTest();
publicvoidf()throwsSimpleException{
thrownewSimpleException();
}
publicstaticvoidmain(String[]args){
SimpleExceptionDemosed=newSimpleExceptionDemo();
try{
sed.f();
}catch(SimpleExceptione){
}
monitor.expect(newString[]{
"ThrowSimpleExceptionfromf()",
"Caughtit!
"
});
}}///:
~
编译器创建了缺省构造器,它将自动调用基类的缺省构造器。
本例中你不会得到像SimpleException(String)这样的构造器,这种构造器也不实用。
你将看到,对异常来说,最重要的部分就是类型的名称,所以本例中建立的异常类在大多数情况下已经够用了。
本例的结果通过System.err打印到控制台的标准错误流。
通常这比把错误信息输出到System.out要好,因为System.out也许会被重定向。
但是把结果送到System.err,它就不会随System.out一起被重定向,这样更容易被用户注意。
你也可以为异常类定义一个接受字符串作为参数的构造器:
//:
c09:
FullConstructors.java
publicMyException(){}
publicMyException(Stringmsg){super(msg);}
}publicclassFullConstructors{
privatestaticTestmonitor=newTest();
publicstaticvoidf()throwsMyException{
thrownewMyException();
}
publicstaticvoidg()throwsMyException{
thrownewMyException("Originateding()");
}
publicstaticvoidmain(String[]args){
try{
f();
}catch(MyExceptione){
e.printStackTrace();
}
try{
g();
}catch(MyExceptione){
e.printStackTrace();
}
monitor.expect(newString[]{
"ThrowingMyExceptionfromf()",
"MyException",
"%%\tatFullConstructors.f\\(.*\\)",
"%%\tatFullConstructors.main\\(.*\\)",
"ThrowingMyExceptionfromg()",
"MyException:
Originateding()",
"%%\tatFullConstructors.g\\(.*\\)",
"%%\tatFullConstructors.main\\(.*\\)"
});
}
}///:
~新增的代码不长:
两个构造器定义了MyException类型对象的创建方式。
对于第二个构造器,使用super关键字明确调用了其基类构造器,它接受一个字符串作为参数。
在异常处理程序中,调用了在Throwable类声明(Exception即从此类继承)的printStackTrace()方法。
它将打印"从方法调用处直到异常抛出处"的方法调用序列。
在缺省情况下,信息将被输出到标准错误流,但你也可以使用重载的版本,把信息输出到任意的流中。
还可以更进一步定义你的异常,比如加入额外的构造器和成员:
//:
c09:
ExtraFeatures.java
//Furtherembellishmentofexceptionclasses.
privateintx;
publicMyException2(){}
publicMyException2(Stringmsg){super(msg);}
publicMyException2(Stringmsg,intx){
super(msg);
this.x=x;
}
publicintval(){returnx;}
publicStringgetMessage(){
return"DetailMessage:
"+x+""+super.getMessage();
}
}publicclassExtraFeatures{
privatestaticTestmonitor=newTest();
publicstaticvoidf()throwsMyException2{
thrownewMyException2();
}
publicstaticvoidg()throwsMyException2{
}
thrownewMyException2("Originateding()");publicstaticvoidh()throwsMyException2{
}try{
f();
}catch(MyException2e){
e.printStackTrace();
}
try{
g();
}catch(MyException2e){
e.printStackTrace();
}
try{
h();
}catch(MyException2e){
e.printStackTrace();
}
monitor.expect(newString[]{
"ThrowingMyException2fromf()",
"MyException2:
DetailMessage:
0null",
"%%\tatExtraFeatures.f\\(.*\\)",
"%%\tatExtraFeatures.main\\(.*\\)",
"ThrowingMyException2fromg()",
"MyException2:
DetailMessage:
0Originateding()",
"%%\tatExtraFeatures.g\\(.*\\)",
"%%\tatExtraFeatures.main\\(.*\\)",
"ThrowingMyException2fromh()",
"MyException2:
DetailMessage:
47Originatedinh()",
"%%\tatExtraFeatures.h\\(.*\\)",
"%%\tatExtraFeatures.main\\(.*\\)",
"e.val()=47"
});
}
}///:
~
thrownewMyException2("Originatedinh()",47);
publicstaticvoidmain(String[]args)
{};
新的异常添加了一个字段i、设定i值的构造器和读取数据的方法。
此外,还重载了Throwable.getMessage()方法,以产生更详细的信息。
对于异常类来说,getMessage()方法有点类似于toString()方法。
既然异常也是对象的一种,所以你可以继续修改这个异常类,以得到更强的功能。
但要记住,使用程序包的客户端程序员可能仅仅只是查看一下抛出的异常类型,其它的就不管了(大多数Java库里的异常都是这么用的),所以你对异常添加的其它功能也许根本用不上。
异常说明Java鼓励你把方法可能会抛出的异常类型,告知使用此方法的客户端程序员。
这是种优雅的做法,它使得调用者能确切知道写什么样的代码可以捕获所有潜在的异常。
当然,如果提供了源代码,客户端程序员可以在源代码中查找throw语句来获知相关信息,然而程序库通常并不与源代码一起发布。
为了预防这样的问题,Java提供了相应的语法(并强制你使用这个语法),使你能以礼貌的方式告知客户端程序员某个方法可能会抛出的异常类型,然后客户端程序员就可以进行相应的处理。
这就是"异常说明"(exceptionspecification),它属于方法声明的一部分,紧跟在形式参数列表之后。
异常说明使用了附加的关键字throws,后面接一个所有潜在异常类型的列表,所以方法定义可能看起来像这样:
voidf()throwsTooBig,TooSmall,DivZero{//...要是你这么写:
voidf(){//...就表示此方法不会抛出任何异常(除了从RuntimeException继承的异常,它们可以在
没有异常说明的情况下被抛出,我们将在后面进行讨论)。
你的代码必须与异常说明保持一致。
如果方法里的代码产生了异常却没有进行处理,编译器会发现这个问题并提醒你:
要么处理这个异常,要么就在异常说明中表明此方法将产生异常。
通过这种自顶向下强制执行的异常说明机制,Java在编译期就可以保证相当程度的异常一致性。
不过还是有个能"作弊"的地方:
你可以声明方法将抛出异常,实际上却不抛出。
编译器相信了你的声明,并强制此方法的用户像真的抛出异常那样使用这个方法。
这样做的好处是,为异常先占了个位子,以后就可以抛出这种异常而不用修改已有的代码。
在定义抽像基类和接口时这种能力很重要,这样派生类或接口实现就能够抛出这些预先声明的异常。
这种在编译期被强制检查的异常称为"被检查的异常"(checkedexception)。
捕获所有异常你可以只写一个异常处理程序来捕获所有类型的异常。
通过捕获异常类型的基类Exception,就可以做到这一点(事实上还有其它的基类,但Exception是同编程活动相关的基类。
):
这将捕获所有异常,所以你最好把它放在处理程序列表的末尾,以防止它抢在其它处理程序之前先把异常捕获了。
因为Exception是与编程有关的所有异常类的基类,所以它不会含有太多特定的信息,不过你可以调用它从Throwable继承的方法:
StringgetMessage()
StringgetLocalizedMessage()
用来获取详细信息,或用本地语言表示的详细信息。
StringtoString()
返回对Throwable的简单描述,要是有详细信息的话,也会把它包含在内。
voidprintStackTrace()
voidprintStackTrace(PrintStream)
打印Throwable和Throwable的调用栈轨迹(callstacktrace)。
调用栈显示了"把
你带到异常抛出地点"的方法调用序列。
此方法第一个版本输出到标准输出流,对后两
个版本你可以选择要输出的流ThrowablefillInStackTrace()用于在Throwable对象的内部记录栈框架(stackframe)的当前状态。
这在程序重新抛出错误或异常(很快就会讲到)时很有用。
此外,你也可以使用Throwable从其基类Object(也是所有类的基类)继承的方法。
对于异常来说,getClass()也许是个很好用的方法,它将返回一个表示此对象类型的对象。
然后你可以使用getName()方法查询这个Class对象的名称。
你还可以用这个
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 异常 错误 处理