异常处理.docx
《异常处理.docx》由会员分享,可在线阅读,更多相关《异常处理.docx(13页珍藏版)》请在冰豆网上搜索。
异常处理
本章介绍Java的异常处理机制。
异常(exception)是在运行时代码序列中产生一种异常情况。
换句话说,异常是一个运行时错误。
在不支持异常处理的计算机语言中,错误必须被手工的检查和处理——典型的是通过错误代码的运用等等。
这种方法既很笨拙也很麻烦。
Java的异常处理避免了这些问题,而且在处理过程中,把运行时错误的管理带到了面向对象的世界。
10.1异常处理基础
Java异常是一个描述在代码段中发生的异常(也就是出错)情况的对象。
当异常情况发生,一个代表该异常的对象被创建并且在导致该错误的方法中被引发(throw)。
该方法可以选择自己处理异常或传递该异常。
两种情况下,该异常被捕获(caught)并处理。
异常可能是由Java运行时系统产生,或者是由你的手工代码产生。
被Java引发的异常与违反语言规范或超出Java执行环境限制的基本错误有关。
手工编码产生的异常基本上用于报告方法调用程序的出错状况。
Java异常处理通过5个关键字控制:
try、catch、throw、throws和finally。
下面讲述它们如何工作的。
程序声明了你想要的异常监控包含在一个try块中。
如果在try块中发生异常,它被抛出。
你的代码可以捕捉这个异常(用catch)并且用某种合理的方法处理该异常。
系统产生的异常被Java运行时系统自动引发。
手动引发一个异常,用关键字throw。
任何被引发方法的异常都必须通过throws子句定义。
任何在方法返回前绝对被执行的代码被放置在finally块中。
下面是一个异常处理块的通常形式:
try{
//blockofcodetomonitorforerrors
}
catch(ExceptionType1exOb){
//exceptionhandlerforExceptionType1
}
catch(ExceptionType2exOb){
//exceptionhandlerforExceptionType2
}
//...
finally{
//blockofcodetobeexecutedbeforetryblockends
}
这里,ExceptionType是发生异常的类型。
下面将介绍怎样应用这个框架。
10.2异常类型
所有异常类型都是内置类Throwable的子类。
因此,Throwable在异常类层次结构的顶层。
紧接着Throwable下面的是两个把异常分成两个不同分支的子类。
一个分支是Exception。
该类用于用户程序可能捕捉的异常情况。
它也是你可以用来创建你自己用户异常类型子类的类。
在Exception分支中有一个重要子类RuntimeException。
该类型的异常自动为你所编写的程序定义并且包括被零除和非法数组索引这样的错误。
另一类分支由Error作为顶层,Error定义了在通常环境下不希望被程序捕获的异常。
Error类型的异常用于Java运行时系统来显示与运行时系统本身有关的错误。
堆栈溢出是这种错误的一例。
本章将不讨论关于Error类型的异常处理,因为它们通常是灾难性的致命错误,不是你的程序可以控制的。
10.3未被捕获的异常
在你学习在程序中处理异常之前,看一看如果你不处理它们会有什么情况发生是很有好处的。
下面的小程序包括一个故意导致被零除错误的表达式。
classExc0{
publicstaticvoidmain(Stringargs[]){
intd=0;
inta=42/d;
}
}
当Java运行时系统检查到被零除的情况,它构造一个新的异常对象然后引发该异常。
这导致Exc0的执行停止,因为一旦一个异常被引发,它必须被一个异常处理程序捕获并且被立即处理。
该例中,我们没有提供任何我们自己的异常处理程序,所以异常被Java运行时系统的默认处理程序捕获。
任何不是被你程序捕获的异常最终都会被该默认处理程序处理。
默认处理程序显示一个描述异常的字符串,打印异常发生处的堆栈轨迹并且终止程序。
下面是由标准javaJDK运行时解释器执行该程序所产生的输出:
java.lang.ArithmeticException:
/byzero
atExc0.main(Exc0.java:
4)
注意,类名Exc0,方法名main,文件名Exc0.java和行数4是怎样被包括在一个简单的堆栈使用轨迹中的。
还有,注意引发的异常类型是Exception的一个名为ArithmeticException
的子类,该子类更明确的描述了何种类型的错误方法。
本章后面部分将讨论,Java提供多个内置的与可能产生的不同种类运行时错误相匹配的异常类型。
堆栈轨迹将显示导致错误产生的方法调用序列。
例如,下面是前面程序的另一个版本,它介绍了相同的错误,但是错误是在main()方法之外的另一个方法中产生的:
classExc1{
staticvoidsubroutine(){
intd=0;
inta=10/d;
}
publicstaticvoidmain(Stringargs[]){
Exc1.subroutine();
}
}
默认异常处理器的堆栈轨迹结果表明了整个调用栈是怎样显示的:
java.lang.ArithmeticException:
/byzero
atExc1.subroutine(Exc1.java:
4)
atExc1.main(Exc1.java:
7)
如你所见,栈底是main的第7行,该行调用了subroutine()方法。
该方法在第4行导致了异常。
调用堆栈对于调试来说是很重要的,因为它查明了导致错误的精确的步骤。
10.4使用try和catch
尽管由Java运行时系统提供的默认异常处理程序对于调试是很有用的,但通常你希望自己处理异常。
这样做有两个好处。
第一,它允许你修正错误。
第二,它防止程序自动终止。
大多数用户对于在程序终止运行和在无论何时错误发生都会打印堆栈轨迹感到很烦恼(至少可以这么说)。
幸运的是,这很容易避免。
为防止和处理一个运行时错误,只需要把你所要监控的代码放进一个try块就可以了。
紧跟着try块的,包括一个说明你希望捕获的错误类型的catch子句。
完成这个任务很简单,下面的程序包含一个处理因为被零除而产生的ArithmeticException异常的try块和一个catch子句。
classExc2{
publicstaticvoidmain(Stringargs[]){
intd,a;
try{//monitorablockofcode.
d=0;
a=42/d;
System.out.println("Thiswillnotbeprinted.");
}catch(ArithmeticExceptione){//catchdivide-by-zeroerror
System.out.println("Divisionbyzero.");
}
System.out.println("Aftercatchstatement.");
}
}
该程序输出如下:
Divisionbyzero.
Aftercatchstatement.
注意在try块中的对println()的调用是永远不会执行的。
一旦异常被引发,程序控制由try块转到catch块。
执行永远不会从catch块“返回”到try块。
因此,“Thiswillnotbeprinted。
”将不会被显示。
一旦执行了catch语句,程序控制从整个try/catch机制的下面一行继续。
一个try和它的catch语句形成了一个单元。
catch子句的范围限制于try语句前面所定义的语句。
一个catch语句不能捕获另一个try声明所引发的异常(除非是嵌套的try语句情况)。
被try保护的语句声明必须在一个大括号之内(也就是说,它们必须在一个块中)。
你不能单独使用try。
构造catch子句的目的是解决异常情况并且像错误没有发生一样继续运行。
例如,下面的程序中,每一个for循环的反复得到两个随机整数。
这两个整数分别被对方除,结果用来除12345。
最后的结果存在a中。
如果一个除法操作导致被零除错误,它将被捕获,a的值设为零,程序继续运行。
//Handleanexceptionandmoveon.
importjava.util.Random;
classHandleError{
publicstaticvoidmain(Stringargs[]){
inta=0,b=0,c=0;
Randomr=newRandom();
for(inti=0;i<32000;i++){
try{
b=r.nextInt();
c=r.nextInt();
a=12345/(b/c);
}catch(ArithmeticExceptione){
System.out.println("Divisionbyzero.");
a=0;//setatozeroandcontinue
}
System.out.println("a:
"+a);
}
}
}
10.4.1显示一个异常的描述
Throwable重载toString()方法(由Object定义),所以它返回一个包含异常描述的字符串。
你可以通过在println()中传给异常一个参数来显示该异常的描述。
例如,前面程序的catch块可以被重写成
catch(ArithmeticExceptione){
System.out.println("Exception:
"+e);
a=0;//setatozeroandcontinue
}
当这个版本代替原程序中的版本,程序在标准javaJDK解释器下运行,每一个被零除错误显示下面的消息:
Exception:
java.lang.ArithmeticException:
/byzero
尽管在上下文中没有特殊的值,显示一个异常描述的能力在其他情况下是很有价值的——特别是当你对异常进行实验和调试时。
10.5使用多重catch语句
某些情况,由单个代码段可能引起多个异常。
处理这种情况,你可以定义两个或更多的catch子句,每个子句捕获一种类型的异常。
当异常被引发时,每一个catch子句被依次检查,第一个匹配异常类型的子句执行。
当一个catch语句执行以后,其他的子句被旁路,执行从try/catch块以后的代码开始继续。
下面的例子设计了两种不同的异常类型:
//Demonstratemultiplecatchstatements.
classMultiCatch{
publicstaticvoidmain(Stringargs[]){
try{
inta=args.length;
System.out.println("a="+a);
intb=42/a;
intc[]={1};
c[42]=99;
}catch(ArithmeticExceptione){
System.out.println("Divideby0:
"+e);
}catch(ArrayIndexOutOfBoundsExceptione){
System.out.println("Arrayindexoob:
"+e);
}
System.out.println("Aftertry/catchblocks.");
}
}
该程序在没有命令行参数的起始条件下运行导致被零除异常,因为a为0。
如果你提供一个命令行参数,它将幸免于难,把a设成大于零的数值。
但是它将导致arrayIndexOutOfBoundsException异常,因为整型数组c的长度为1,而程序试图给c[42]赋值。
下面是运行在两种不同情况下程序的输出:
C:
\>javaMultiCatch
a=0
Divideby0:
java.lang.ArithmeticException:
/byzero
Aftertry/catchblocks.
C:
\>javaMultiCatchTestArg
a=1
Arrayindexoob:
java.lang.ArrayIndexOutOfBoundsException
Aftertry/catchblocks.
当你用多catch语句时,记住异常子类必须在它们任何父类之前使用是很重要的。
这是因为运用父类的catch语句将捕获该类型及其所有子类类型的异常。
这样,如果子类在父类后面,子类将永远不会到达。
而且,Java中不能到达的代码是一个错误。
例如,考虑下面的程序:
/*Thisprogramcontainsanerror.
Asubclassmustcomebeforeitssuperclassin
aseriesofcatchstatements.Ifnot,
unreachablecodewillbecreatedanda
compile-timeerrorwillresult.
*/
classSuperSubCatch{
publicstaticvoidmain(Stringargs[]){
try{
inta=0;
intb=42/a;
}catch(Exceptione){
System.out.println("GenericExceptioncatch.");
}
/*Thiscatchisneverreachedbecause
ArithmeticExceptionisasubclassofException.*/
catch(ArithmeticExceptione){//ERROR-unreachable
System.out.println("Thisisneverreached.");
}
}
}
如果你试着编译该程序,你会收到一个错误消息,该错误消息说明第二个catch语句不会到达,因为该异常已经被捕获。
因为ArithmeticException是Exception的子类,第一个catch语句将处理所有的面向Exception的错误,包括ArithmeticException。
这意味着第二个catch语句永远不会执行。
为修改程序,颠倒两个catch语句的次序。
10.6嵌套try语句
Try语句可以被嵌套。
也就是说,一个try语句可以在另一个try块内部。
每次进入try语句,异常的前后关系都会被推入堆栈。
如果一个内部的try语句不含特殊异常的catch处理程序,堆栈将弹出,下一个try语句的catch处理程序将检查是否与之匹配。
这个过程将继续直到一个catch语句匹配成功,或者是直到所有的嵌套try语句被检查耗尽。
如果没有catch语句匹配,Java的运行时系统将处理这个异常。
下面是运用嵌套try语句的一个例子:
//Anexampleofnestedtrystatements.
classNestTry{
publicstaticvoidmain(Stringargs[]){
try{
inta=args.length;
/*Ifnocommand-lineargsarepresent,
thefollowingstatementwillgenerate
adivide-by-zeroexception.*/
intb=42/a;
System.out.println("a="+a);
try{//nestedtryblock
/*Ifonecommand-lineargisused,
thenadivide-by-zeroexception
willbegeneratedbythefollowingcode.*/
if(a==1)a=a/(a-a);//divisionbyzero
/*Iftwocommand-lineargsareused,
thengenerateanout-of-boundsexception.*/
if(a==2){
intc[]={1};
c[42]=99;//generateanout-of-boundsexception
}
}catch(ArrayIndexOutOfBoundsExceptione){
System.out.println("Arrayindexout-of-bounds:
"+e);
}
}catch(ArithmeticExceptione){
System.out.println("Divideby0:
"+e);
}
}
}
如你所见,该程序在一个try块中嵌套了另一个try块。
程序工作如下:
当你在没有命令行参数的情况下执行该程序,外面的try块将产生一个被零除的异常。
程序在有一个命令行参数条件下执行,由嵌套的try块产生一个被零除的错误。
因为内部的块不匹配这个异常,它将把异常传给外部的try块,在那里异常被处理。
如果你在具有两个命令行参数的条件下执行该程序,由内部try块产生一个数组边界异常。
下面的结果阐述了每一种情况:
C:
\>javaNestTry
Divideby0:
java.lang.ArithmeticException:
/byzero
C:
\>javaNestTryOne
a=1
Divideby0:
java.lang.ArithmeticException:
/byzero
C:
\>javaNestTryOneTwo
a=2
Arrayindexout-of-bounds:
java.lang.ArrayIndexOutOfBoundsException
当有方法调用时,try语句的嵌套可以很隐蔽的发生。
例如,你可以把对方法的调用放在一个try块中。
在该方法内部,有另一个try语句。
这种情况下,方法内部的try仍然是嵌套在外部调用该方法的try块中的。
下面是前面例子的修改,嵌套的try块移到了方法nesttry()的内部:
/*Trystatementscanbeimplicitlynestedvia
callstomethods.*/
classMethNestTry{
staticvoidnesttry(inta){
try{//nestedtryblock
/*Ifonecommand-lineargisused,
thenadivide-by-zeroexception
willbegeneratedbythefollowingcode.*/
if(a==1)a=a/(a-a);//divisionbyzero
/*Iftwocommand-lineargsareused,
thengenerateanout-of-boundsexception.*/
if(a==2){
intc[]={1};
c[42]=99;//generateanout-of-boundsexception
}
}catch(ArrayIndexOutOfBoundsExceptione){
System.out.println("Arrayindexout-of-bounds:
"+e);
}
}
publicstaticvoidmain(Stringargs[]){
try{
inta=args.length;
/*Ifnocommand-lineargsarepresent,
thefollowingstatementwillgenerate
adivide-by-zeroexception.*/
intb=42/a;
System.out.println("a="+a);
nesttry(a);
}catch(ArithmeticExceptione){
System.out.println("Divideby0:
"+e);
}
}
}
该程序的输出与前面的例子相同。
10.7引发(throw)
到目前为止,你只是获取了被Java运行时系统引发的异常。
然而,程序可以用throw语句引发明确的异常。
Throw语句的通常形式如下:
throwThrowableInstance;
这里,ThrowableInstance一定是Throwable类类型或Throwable子类类型的一个对象。
简单类型,例如int或char,以及非Throwable类,例如String或Object,不能用作异常。
有两种可以获得Throwable对象的方法:
在catch子句中使用参数或者用new操作符创建。
程序执行在throw语句之后立即停止;后面的任何语句不被执行。
最紧紧包围的try块用来检查它是否含有一个与异常类型匹配的catch语句。
如果发现了匹配的块,控制转向该语句;如果没有发现,次包围的try块来检查,以此类推。
如果没有发现匹配的catch块,默认异常处理程序中断程序的执行并且打印堆栈轨迹。
下面是一个创建并引发异常的例子程序,与异常匹配的处理程序再把它引发给外层的处理程序。
//Demonstratethrow.
classThrowDemo{
staticvoiddemoproc(){
try{
thrownewNullPointerException("demo");
}catch(NullPointerExceptione){
System.out.println("Caughtinsidedemoproc.");
throwe;//rethrowtheexception
}
}
publicstaticvoidmain(Stringargs[]){
try{
demoproc();
}catch(NullPointerExceptione){
System.out.println("Recaught:
"+e);
}
}
}
该程序有两个机会处理相同的错误。
首先,main()设立了一个异常关系然后调用demoproc()方法,然后设立了另一个异常处理关系并且立即引发一个新的NullPointerException实例,NullPointerException在下一行被捕获。
异常于是被再次引发。
下面是输出结果:
Caughtinsidedemoproc.
Recaught:
java.lang.NullPointerException:
demo
该程序还阐述了怎样