Java异常是一个描述在代码段中发生的异常(也就是出错)情况的对象。当异常情况发生,一个代表该异常的对象被创建并且在导致该错误的方法中被引发(throw)。该方法可以选择自己处理异常或传递该异常。两种情况下,该异常被捕获(catch)并处理。异常可能是由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是发生异常的类型。下面将介绍怎样应用这个框架。
异常类型 |
所有异常类型都是内置类Throwable的子类。因此,Throwable在异常类层次结构的顶层。紧接着Throwable下面的是两个把异常分成两个不同分支的子类。一个分支是Exception。该类用于用户程序可能捕捉的异常情况。它也是你可以用来创建你自己用户异常类型子类的类。
在Exception分支中有一个重要子类RuntimeException。该类型的异常自动为你所编写的程序定义并且包括被零除和非法数组索引这样的错误。另一类分支由Error作为顶层,Error定义了在通常环境下不希望被程序捕获的异常。Error类型的异常用于Java运行时系统来显示与运行时系统本身有关的错误。堆栈溢出是这种错误的一例。本章将不讨论关于Error类型的异常处理,因为它们通常是灾难性的致命错误,不是你的程序可以控制的。
未被捕获的异常 |
在你学习在程序中处理异常之前,看一看如果你不处理它们会有什么情况发生是很有好处的。下面的小程序包括一个故意导致被零除错误的表达式。
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行导致了异常。调用堆栈对于调试来说是很重要的,因为它查明了导致错误的精确的步骤。
使用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);
}
}
}
显示一个异常的描述 |
Throwable重载toString()方法(由Object定义),所以它返回一个包含异常描述的字符串。你可以通过在println()中传给异常一个参数来显示该异常的描述。例如,前面程序的catch块可以被重写成
catch(ArithmeticExceptione){
System.out.println("Exception:"+e);
a=0;//setatozeroandcontinue
}
当这个版本代替原程序中的版本,程序在标准javaJDK解释器下运行,每一个被零除错误显示下面的消息:
Exception:java.lang.ArithmeticException:/byzero
尽管在上下文中没有特殊的值,显示一个异常描述的能力在其他情况下是很有价值的——特别是当你对异常进行实验和调试时。
使用多重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.
Asubclassmustcomebeforeitssuperclassinaseriesofcatchstatements.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语句的次序。
嵌套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);
}
}
}
该程序的输出与前面的例子相同。
引发(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()。demoproc()方法然后设立了另一个异常处理关系并且立即引发一个新的NullPointerException实例,NullPointerException在下一行被捕获。异常于是被再次引发。下面是输出结果:
Caughtinsidedemoproc.
Recaught:java.lang.NullPointerException:demo
该程序还阐述了怎样创建Java的标准异常对象,特别注意下面这一行:
thrownewNullPointerException("demo");
这里,new用来构造一个NullPointerException实例。所有的Java内置的运行时异常有两个构造函数:一个没有参数,一个带有一个字符串参数。当用到第二种形式时,参数指定描述异常的字符串。如果对象用作print()或println()的参数时,该字符串被显示。这同样可以通过调用getMessage()来实现,getMessage()是由Throwable定义的。
throws |
如果一个方法可以导致一个异常但不处理它,它必须指定这种行为以使方法的调用者可以保护它们自己而不发生异常。做到这点你可以在方法声明中包含一个throws子句。一个throws子句列举了一个方法可能引发的所有异常类型。这对于除Error或RuntimeException及它们子类以外类型的所有异常是必要的。一个方法可以引发的所有其他类型的异常必须在throws子句中声明。如果不这样做,将会导致编译错误。
下面是包含一个throws子句的方法声明的通用形式:
typemethod-name(parameter-list)throwsexception-list
{
//bodyofmethod
}
这里,exception-list是该方法可以引发的以有逗号分割的异常列表。
下面是一个不正确的例子。该例试图引发一个它不能捕获的异常。因为程序没有指定一个throws子句来声明这一事实,程序将不会编译。
//Thisprogramcontainsanerrorandwillnotcompile.
classThrowsDemo{
staticvoidthrowOne(){
System.out.println("InsidethrowOne.");
thrownewIllegalAccessException("demo");
}
publicstaticvoidmain(Stringargs[]){
throwOne();
}
}
为编译该程序,需要改变两个地方。第一,需要声明throwOne()引发IllegalAccessException异常。第二,main()必须定义一个try/catch语句来捕获该异常。
正确的例子如下:
//Thisisnowcorrect.
classThrowsDemo{
staticvoidthrowOne()throwsIllegalAccessException{
System.out.println("InsidethrowOne.");
thrownewIllegalAccessException("demo");
}
publicstaticvoidmain(Stringargs[]){
try{
throwOne();
}catch(IllegalAccessExceptione){
System.out.println("Caught"+e);
}
}
}
下面是例题的输出结果:
insidethrowOne
caughtjava.lang.IllegalAccessException:demo
finally |
当异常被引发,通常方法的执行将作一个陡峭的非线性的转向。依赖于方法是怎样编码的,异常甚至可以导致方法过早返回。这在一些方法中是一个问题。例如,如果一个方法打开一个文件项并关闭,然后退出,你不希望关闭文件的代码被异常处理机制旁路。finally关键字为处理这种意外而设计。finally创建一个代码块。该代码块在一个try/catch块完成之后另一个try/catch出现之前执行。finally块无论有没有异常引发都会执行。如果异常被引发,finally甚至是在没有与该异常相匹配的catch子句情况下也将执行。
一个方法将从一个try/catch块返回到调用程序的任何时候,经过一个未捕获的异常或者是一个明确的返回语句,finally子句在方法返回之前仍将执行。这在关闭文件句柄和释放任何在方法开始时被分配的其他资源是很有用的。finally子句是可选项,可以有也可以无。然而每一个try语句至少需要一个catch或finally子句。
下面的例子显示了3种不同的退出方法。每一个都执行了finally子句:
//Demonstratefinally.
classFinallyDemo{
//Throughanexceptionoutofthemethod.
staticvoidprocA(){
try{
System.out.println("insideprocA");
thrownewRuntimeException("demo");
}finally{
System.out.println("procA'sfinally");
}
}
//Returnfromwithinatryblock.
staticvoidprocB(){
try{
System.out.println("insideprocB");
return;
}finally{
System.out.println("procB'sfinally");
}
}
//Executeatryblocknormally.
staticvoidprocC(){
try{
System.out.println("insideprocC");
}finally{
System.out.println("procC'sfinally");
}
}
publicstaticvoidmain(Stringargs[]){
try{
procA();
}catch(Exceptione){
System.out.println("Exceptioncaught");
}
procB();
procC();
}
}
该例中,procA()过早地通过引发一个异常中断了try。Finally子句在退出时执行。procB()的try语句通过一个return语句退出。在procB()返回之前finally子句执行。在procC()中,try语句正常执行,没有错误。然而,finally块仍将执行。
注意:如果finally块与一个try联合使用,finally块将在try结束之前执行。
下面是上述程序产生的输出:
insideprocA
procA’sfinally
Exceptioncaught
insideprocB
procB’sfinally
insideprocC
procC’sfinally
Java的内置异常 |
在标准包java.lang中,Java定义了若干个异常类。前面的例子曾用到其中一些。这些异常一般是标准类RuntimeException的子类。因为java.lang实际上被所有的Java程序引入,多数从RuntimeException派生的异常都自动可用。而且,它们不需要被包含在任何方法的throws列表中。
Java语言中,这被叫做未经检查的异常(uncheckedexceptions)。因为编译器不检查它来看一个方法是否处理或引发了这些异常。java.lang中定义的未经检查的异常列于表10-1。表10-2列出了由java.lang定义的必须在方法的throws列表中包括的异常,如果这些方法能产生其中的某个异常但是不能自己处理它。这些叫做受检查的异常(checkedexceptions)。Java定义了几种与不同类库相关的其他的异常类型。
表8-1Java的java.lang中定义的未检查异常子类
表8-2java.lang中定义的检查异常
创建自己的异常子类 |
尽管Java的内置异常处理大多数常见错误,你也许希望建立你自己的异常类型来处理你所应用的特殊情况。这是非常简单的:只要定义Exception的一个子类就可以了(Exception当然是Throwable的一个子类)。你的子类不需要实际执行什么——它们在类型系统中的存在允许你把它们当成异常使用。
Exception类自己没有定义任何方法。当然,它继承了Throwable提供的一些方法。因此,所有异常,包括你创建的,都可以获得Throwable定义的方法。这些方法显示在表8-3中。你还可以在你创建的异常类中覆盖一个或多个这样的方法。
表8-3Throwable定义的方法
下面的例子声明了Exception的一个新子类,然后该子类当作方法中出错情形的信号。
它重载了toString()方法,这样可以用println()显示异常的描述。
//Thisprogramcreatesacustomexceptiontype.
classMyExceptionextendsException{
privateintdetail;
MyException(inta){
detail=a;
}
publicStringtoString(){
return"MyException["+detail+"]";
}
}
classExceptionDemo{
staticvoidcompute(inta)throwsMyException{
System.out.println("Calledcompute("+a+")");
if(a>10)
thrownewMyException(a);
System.out.println("Normalexit");
}
publicstaticvoidmain(Stringargs[]){
try{
compute(1);
compute(20);
}catch(MyExceptione){
System.out.println("Caught"+e);
}
}
}
该例题定义了Exception的一个子类MyException。该子类非常简单:它只含有一个构造函数和一个重载的显示异常值的toString()方法。ExceptionDemo类定义了一个compute()方法。该方法引发一个MyException对象。当compute()的整型参数比10大时该异常被引发。
main()方法为MyException设立了一个异常处理程序,然后用一个合法的值和不合法的值调用compute()来显示执行经过代码的不同路径。下面是结果:
Calledcompute(1)
Normalexit
Calledcompute(20)
CaughtMyException[20]
使用异常 |
异常处理为控制具有很多动态运行时特性的复杂程序提供了一个强大的机制。把try,throw,和catch当成处理错误简洁及程序逻辑上的反常边界条件是很重要的。如果你像多数程序员一样,那么你可能习惯于在方法失败时返回一个错误代码。在你用Java编程时,你应该打破这个习惯。当方法可能失败时,引发一个异常。这是处理失败模式的一个更简洁的方法。
最后说明一点:Java的异常处理语句不应该被当作是一个非本地分支的通常机制,如果你这样认为,它将困扰你的代码并使代码难于维护。