Java的异常处理机制
-
-
-
- 1.1 异常处理的基础知识
- 1.2 异常类型
- 1.3 未捕获的异常
- 1.4 使用 try 和 catch
- 1.5 多条catch子句
- 1.6 嵌套的 try 语句
- 1.7 throw
- 1.8 throws
- 1.9 finally
- 1.10 Java的内置异常
- 1.11 创建自己的异常子类
- 1.12 链式异常
- 1.13 从JDK7开始添加的3个异常特性
- 1.14 使用异常
-
-
本篇介绍Java的异常处理机制。异常是运行时在代码序列中引起的非正常状况。换句话说,异常是运行时错误。在不支持异常处理的计算机语言中,必须手动检查和处理错误——通常通过使用错误代码,等等。这种方式既笨拙又麻烦。Java的异常处理避免了这些问题,并且在处理过程中采用面向对象的方式管理运行时错误。
1.1 异常处理的基础知识
Java异常是用来描述在一段代码中发生的异常情况(也就是错误)的对象。当出现引起异常的情况时,就会创建用来表示异常的对象,并在引起错误的方法中抛出异常对象。方法可以选择自己处理异常,也可以继续传递异常。无论采用哪种方式,在某一点都会捕获并处理异常。异常可以由Java运行时系统生成,也可以通过代码手动生成。由Java抛出的异常与那些违反Java语言规则或Java执行环境约束的基础性错误有关。手动生成的异常通常用于向方法的调用者报告某些错误条件。
Java异常处理通过5个关键字进行管理:try、catch、throw、throws以及finally。下面简要介绍它们的工作原理。如果在try代码块中发生异常,就会将异常抛出。代码可以(使用catch)捕获异常,并以某些理性方式对其进行处理。系统生成的异常通常由Java运行时系统自动抛出。为了手动抛出异常,需要使用throw关键字。从方法抛出的任何异常都必须通过一条throws子句进行指定。在try代码块结束之后必须执行的所有代码都需要放入finally代码快中。
下面是异常处理代码块的一般形式:
try {
//block of code to monitor for errors
}
catch (ExceptionType1 exOb){
// exception handler for ExcepionType1
}
catch (ExceptionType2 exOb){
// exception handler for ExcepionType2
}
//...
finally {
//block of code to be executed after try block ends
}
其中,ExceptionType是已经发生的异常的类型。
注意:
从JDK7开始,try语句增加了一种支持自动资源管理的新形式,这种形式的try被称为带资源的try(文件是最常用的资源)。
1.2 异常类型
所有异常类型都是内置类Throwable的子类。因此,Throwable位于异常类层次的顶部。紧随Throwable之下的是两个子类,它们将异常分为两个不同的分支。一个分支是Exception类,这个类既可以用于用户程序应当捕获的异常情况,也可以用于创建自定义异常类型的子类。Exception有一个重要子类,名为RuntimeException。对于我们编写的程序而言,这种类型的异常是自动定义的,包括除零和无效数组索引这类情况。
另外一个分支是Error类,该类定义了在常规环境下不希望由程序捕获的异常。Error类型的异常由Java运行时系统使用,以指示运行时环境本身出现了某些错误。堆栈溢出是这类错误的一个例子。由于它们通常是为了响应灾难性的失败而创建的,我们的程序不能处理这类异常。
顶级的异常层次如下图所示:
1.3 未捕获的异常
学习如何在程序中处理异常之前,先看一看如果不处理异常会发生什么情况,这将是有用的。下面的小程序包含一个故意引起除零错误的表达式:
class Exc0 {
public static void main(String args[]){
int d = 0;int a = 42 / d;}
}
当Java运行时系统检测到试图除以零时,它会构造一个新的异常对象,然后抛出这个异常。这会导致Exc0终止执行,因为一旦抛出异常,就必须有一个异常处理程序捕获该异常,并立即进行处理。在这个例子中,没有提供任何自己的异常处理程序,所以该异常会由Java运行时系统提供的默认处理程序捕获。没有被程序捕获的所有异常,最终都将由默认处理程序进行处理。默认处理程序会显示一个描述异常的字符串,输出异常发生点的堆栈踪迹并终止程序。
下面是执行这个程序时发生的异常:
java.lang.ArithmeticException: / by zero at Exc0.main(Exc0.java:4)
注意类名(Exc0)、方法名(main)、文件名(Exc0.java)以及行号(4),是如何都被包含到这个简单的堆栈踪迹中的。此外,注意抛出异常的类型是Exception的子类ArithmeticException,该类更具体地描述了发生的错误类型。
堆栈踪迹总是会显示导致错误的方法调用序列。例如,下面是前面程序的另一个版本,该版本在与main()相互独立的一个方法中引入了相同的错误。
class Exc1 {
static void subroutine(){
int d = 0;int a = 10 / d;}public static void main(String args[]){
Exc1.subroutine();}
}
默认异常处理程序产生的堆栈踪迹显示了整个调用堆栈的过程。如下所示:
java.lang.ArithmeticException: / by zero at Exc1.subroutine(Exc1.java:4) at Exc1.main(Exc1.java:7)
可以看出,堆栈的底部是main()方法中的第7行,该行调用subroutine()方法,该方法在第4行引起了异常。调用堆栈对于调试非常有用,因为可以精确定位导致错误发生的步骤序列。
1.4 使用 try 和 catch
尽管对于调试而言,Java运行时系统提供的默认异常处理程序很有用,但是通常希望自己处理异常。自己处理异常有两个优点:第一,允许修复错误;第二,阻止程序自动终止。如果程序停止运行并且无论何时发生错误都输出堆栈踪迹,会让大多数用户感到困惑!幸运的是,防止这种情况的发生很容易。
为了防止并处理运行时错误,可以简单地在try代码块中封装希望监视的代码。紧随try代码块之后,提供一条catch子句,指定希望捕获的异常类型。下面的程序提供了一个try代码和一条catch子句,它们处理由除零错误引起的ArithmeticException异常:
class Exc2 {
public static void main(String args[]){
int d,a;try{
//monitor a block of coded = 0;a = 42 / d;System.out.println("This will not be printed.");}catch(ArithmeticException e){
//catch divide-by-zero errorSystem.out.println("Division by zero.");}System.out.println("After catch statement.");/*输出结果:Division by zero.After catch statement.*/}
}
注意,对try代码中println()方法的调用永远都不会执行。一旦抛出异常,程序控制就会从try代码块中转移出来,进入catch代码块中。换句话说,不是“调用”catch,所以执行控制永远不会从catch代码块“返回”到try代码块。因此,不会显示"This will not be printed."这一行。执行完catch语句后,程序控制就会继续进入到程序中整个try/catch代码块的下一行。
try及其catch语句构成了一个单元。catch子句的作用域被限制在由之前try语句执行的那些语句内。catch语句不能捕获由另一条try语句抛出的异常(这种情况的一个例外是嵌套的try语句)。由try保护的语句必须使用花括号括起来(也就是说,它们位于一个代码块中)。不能为单条语句使用try。
大部分设计良好的catch子句,都应当能够分辨出异常情况,然后继续执行,就好像错误根本没有发生一样。例如下面的程序中,for循环的每次迭代都会获取两个随机整数。将两个整数彼此相除,之后用12345除以结果。将最终结果保存到变量a中。只要任何一个除法操作引起除零错误,就会捕获该错误,并将a设置为0,然后程序继续执行。
//Handle an exception and move on
import java.util.Random;
class HandelError{
public static void main(String args[]){
int a=0,b=0,c=0;Random r = new Random();for(int i=0;i<32000;i++){
try{
b = r.nextInt();c = r.nextInt();a = 12345 / (b/c);}catch(ArithmeticException){
System.out.println("Division by zero.");a = 0;//set a to zero and continue}System.out.println("a:"+a);}}
}
显示异常的描述信息
Throwable重写了(由Object定义的)toString()方法,从而可以返回一个包含异常描述的字符串。可以使用println()语句显示这个描述,为此,只需要简单地将异常作为参数传递为println()方法。可以将上面的程序中的catch块改写成如下形式:
catch (ArithmeticException e) {
System.out.println("Exception:" + e);a = 0;//set a to zero and continue
}
如果将程序替换成这个版本并运行程序,那么每个除零错误都会显示下面的消息:
Exception:java.lang.ArithmeticException: / by zero
这样做虽然在这个例子中不是很有价值,但是显示异常描述的能力对于其他情况却是很有价值的——特别是当对异常进行试验或进行调试时。
1.5 多条catch子句
在有些情况下,单块代码可能会引发多个异常。为了处理这种情况,可以指定两条或多条catch语句,每条catch子句捕获不同类型的异常。当抛出异常时,按顺序检查每条catch语句,并执行类型和异常能够匹配的第一条catch子句。执行了一条catch语句之后,会忽略其他catch语句,并继续执行try/catch代码块后面的代码。下面的例子捕获了两种不同的异常类型:
//Demonstrate multiple catch statements
class MultipleCatches {
public static void main(String args[]) {
try{
int a = args.length;System.out.println("a = "+a);int b = 42 / a;int c[] = {
1};c[42] = 99;}catch(ArithmeticException e){
System.out.println("Divide by 0: "+e);}catch(ArrayIndexOutOfBoundsException e){
System.out.println("Array index oob: "+e);}System.out.println("After try/catch blocks.");}
}
如果在启动程序时没有提供命令行参数,那么上述程序会引起除零异常,因为a会等于0。如果提供命令行参数,那么会将a设置为大于0的数值,程序会继续进行除法运算。但是会引起ArrayIndexOutOfBoundsException异常,因为int型数组c的长度为1,但是程序视图为c[42]赋值。
下面是以下两种方式运行上述程序后生成的输出:
C:\java MultipleCatches
a = 0
Devide by 0: java.lang.ArithmeticException: /by zero
After try/catch blocks.
C:\java MultipleCatches TestArgs
a = 1;
Array index oob: java.lang.ArrayIndexOutOfBoundsException:42
After try/catch blocks.
当使用多条catch语句时,要重点记住异常子类必须位于所有超类之前,因为使用了某个超类的catch语句会捕获这个超类及其所有子类的异常。因此,如果子类位于超类之后的话,永远也不会到达子类。此外在Java中,不可到达的代码被认为是错误。例如分析下面的程序:
/* This program contains an error. A subclass must come before its superclass in a series of catch statements. If not,unreachable code will be created and a complie-time error will result. */
public class SuperSubCatch {
public static void main(String[] args) {
try {
int a = 0;int b = 42 / a;} catch (Exception e) {
System.out.println("Generic Exception catch.");}/*This catch is never reached beacuse ArithmeticException is a subclass of Exception*/ catch (ArithmeticException e) {
//Exception 'java.lang.ArithmeticException' has already been caughtSystem.out.println("This is never reached.");}}
}
Idea中编译此程序报错:Exception ‘java.lang.ArithmeticException’ has already been caught。因为ArithmeticException是Exception的子类,所以第一条catch语句会处理所有基于Exception的错误,包括ArithmeticException。这意味着永远不会执行第二条catch语句。为了解决这个问题,可以颠倒这两条catch语句的次序。
1.6 嵌套的 try 语句
可以嵌套try语句。也就是说,一条try语句可以位于另外一条try语句中。每次遇到try语句时,异常的上下文就会被推入到堆栈中。如果内层的try语句没有为特定的异常提供catch处理程序,堆栈就会弹出该try语句,检查下一条try语句的catch处理程序,查看是否匹配异常。这个程序会一直继续下去,直到找到一条匹配的catch语句,或直到检查完所有嵌套的try语句。如果没有找到匹配的catch语句,Java运行时系统会处理异常。下面是一个使用嵌套try语句的例子:
//An example of nested try statements.
class NestTry {
public static void main(String[] args) {
try {
int a = args.length;/*If no common-line args are parent,the following statement will generate a divide-by-zero exception.*/int b = 42 / a;System.out.println("a = " + a);try {
//nested try block/*If one common-line arg is used,then a divide-by-zero exception will be generated by the following code.*/if (a == 1) a = a / (a - a);/*If two common-line args are used,then generate an out-of-bounds exception.*/if (a == 2) {
int c[] = {
1};c[42] = 99;//generate an out-of-bounds exception }} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Array index out-of-bounds: " + e);}} catch (ArithmeticException e) {
System.out.println("Divide by 0: " + e);}}
}
可以看出,该程序在一个try代码块中嵌套了另外一个try代码块。该程序的工作过程如下:如果执行程序时没有提供命令行参数,那么外层的try代码块会产生除零异常。如果执行程序时提供了一个命令行参数,那么会从嵌套的try代码块中产生除零异常。因为内层的try代码块没有捕获该异常,所以会将其传递到外层的try代码块,在此对除零异常进行处理。如果执行程序时提供了两个命令行参数,那么会从内层的try代块中产生数组越界异常。下面的示例运行演示了各种情况:
C:\java NestTry
Devide by 0: java.lang.ArithmeticException: /by zero
C:\java NestTry one
a = 1;
Devide by 0: java.lang.ArithmeticException: /by zero
C:\java NestTry one Two
a = 2;
Array index out-of-bounds: java.lang.ArrayIndexOfBoundsException:42
当涉及方法调用时,可能会出现不那么明显的try语句嵌套。例如,可能在一个try代码块中包含了对某个方法的调用,而在该方法内部又有另外一条try语句。对于这种情况,方法内部的try语句仍然被嵌套在调用该方法的外层try代码块中。下面是对上一个程序重新进行了编码,将嵌套的try代码块移到了nesttry()方法中:
/* Try statements can be implicitly nested via calls to methods.*/
class MethNestTry {
static void nesttry(int a) {
try {
//nested try block/*If one common-line arg is used,then a divide-by-zero exception will be generated by the following code.*/if (a == 1) a = a / (a - a);/*If two common-line args are used,then generate an out-of-bounds exception.*/if (a == 2) {
int c[] = {
1};c[42] = 99;//generate an out-of-bounds exception}} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Array index out-of-bounds: " + e);}}public static void main(String[] args) {
try {
int a = args.length;/*If no common-line args are parent,the following statement will generate a divide-by-zero exception.*/int b = 42 / a;System.out.println("a = " + a);nesttry(a);} catch (ArithmeticException e) {
System.out.println("Divide by 0: " + e);}}
}
该程序的输出和上一版本的完全相同
1.7 throw
到目前为止,捕获的都是由Java运行时系统抛出的异常。但是,我们的程序也可以使用throw语句显式地抛出异常。throw的一般形式如下所示:
throw ThrowableInstance;
其中,ThrowableInstance必须是Throwable或其子类类型的对象。基本类型(如int或char)以及非Throwable类(如String和Object)都不能用作异常。可以通过两种方式获得Throwable对象:在catch子句中使用参数或者使用new运算符创建Throwable对象。
throw语句之后的执行流会立即停止,所有后续语句都不会执行。检查最近的try代码块,查看是否存在和异常类型相匹配的catch语句。如果没有,就检查下一条try语句,这个过程一直重复下去。如果最终没有找到匹配的catch语句,那么默认的异常处理程序会终止程序并输出堆栈踪迹。
下面是一个创建并抛出异常的示例程序。捕获异常的处理程序将异常再次抛出到外层的异常处理程序中:
//Demonstrate throw
public class ThrowDemo {
static void demoproc() {
try {
throw new NullPointerException("demo");} catch (NullPointerException e) {
System.out.println("Caught inside demoproc.");throw e;//rethrow the exception}}public static void main(String[] args) {
try {
demoproc();} catch (NullPointerException e) {
System.out.println("Recaught:" + e);}}
}
该程序有两次处理相同错误的机会。首先,main()方法设置了一个异常上下文,然后调用demoproc()方法。接下来demoproc()方法设置了另外一个异常上下文,并立即抛出新的NullPointerException异常,该异常在下一行被捕获。然后再次抛出异常。下面是产生的输出:
Caught inside demoproc.
Recaught:java.lang.NullPointerException: demo
该程序还演示了如何创建Java的标准异常对象。请注意下面这行代码:
throw new NullPointerException(“demo”);
在此,使用new构造了一个NullPointerException实例。许多内置的Java运行时异常至少有两个构造函数:一个不带参数,另一个带有一个字符串参数。如果使用第二种方式,那么参数指定了用来描述异常的字符串。当将对象用作print()和println()方法的参数时,会显示该字符串。还可以通过调用getMessage()方法获取这个字符串,该方法是由Throwable定义的。
1.8 throws
如果方法可能引发自身不进行处理的异常,就必须指明这种行为,以便方法的调用者能够保卫他们自己以防备上述异常。可以通过在方法声明中提供throws子句来完成该任务。throws子句列出了方法可能抛出的异常类型。除了Error和RuntimeException及其子类类型的异常之外,对于所有其他类型的异常这都是必需的。方法可能抛出的所有其他异常都必须在throws子句中进行声明。如果没有这么做,就会产生编译时错误。
下面是包含throws子句的方法声明的一般形式:
type method-name(parameter-list) throws exception-list
{
//body of method
}
下面是一个错误程序,该程序试图抛出无法匹配的异常。因为程序没有指定throws子句来声明这一事实,所以程序无法编译。
//This program contains an error and will not complie
class ThrowsDemo {
static void throwOne(){
System.out.println("Inside throwOne.");throw new IllegalAccessException("demo");}public static void main(String args[]){
throwOne();}
}
为了使这个例子能够编译,需要进行两处修改。首先,需要声明throwOne()方法抛出IllegalAccessException异常。其次,main()方法必须定义捕获该异常的try/catch语句。
//This is now correct
class ThrowsDemo {
static void throwOne() throws IllegalAccessException {
System.out.println("Inside throwOne.");throw new IllegalAccessException("demo");}public static void main(String args[]) {
try {
throwOne();} catch (IllegalAccessException e) {
System.out.println("Caught " + e);}/*输出结果:Inside throwOne.Caught java.lang.IllegalAccessException: demo*/}
}
1.9 finally
当抛出异常后,方法中的执行流会采用一条非常突然。非线性的路径,这将改变方法的正常执行流程。根据方法的编码方式,异常甚至可能使方法比预期时间更早地返回。对于某些方法,这可能是一个问题。例如,如果方法在开始时打开一个文件,并在结束时关闭这个文件,那么您可能不希望关闭文件的代码绕过异常处理机制。关键字finally就是为了解决这种可能情况而设计的。
使用finally可以创建一个代码块,该代码块会在执行try/catch代码块之后,并在执行try/catch代码块后面的代码之前执行。不管是否有异常抛出,都会执行finally代码块。如果抛出了异常,那么即使没有catch语句能匹配异常,finally代码块也仍将执行。只要方法从try/catch代码块内部返回到调用者,不管是通过未捕获的异常还是使用显示的返回语句,都会在方法返回之前执行finally子句。对于关闭文件句柄以及释放在方法开始时进行分配,并在方法返回之前进行处理的其他资源来说,finally子句都很有用的。finally子句是可选的。但是,每条try语句至少需要有一条catch子句或finally子句。
下面的示例程序显示了以各种方式退出的三个方法,所有这些方法都执行它们各自的finally子句:
//Demonstrate finally
public class FinallyDemo {
//Throw an exception out of the method.static void procA() {
try {
System.out.println("inside procA");throw new RuntimeException("demo");} finally {
System.out.println("procA's finally");}}//Return from within a try blockstatic void procB() {
try {
System.out.println("inside procB");return;} finally {
System.out.println("procB's finally");}}//Execute a try block normallystatic void procC() {
try {
System.out.println("inside procC");} finally {
System.out.println("procC's finally");}}public static void main(String[] args) {
try {
procA();} catch (Exception e) {
System.out.println("Exception caught");}procB();procC();/*输出结果:inside procAprocA's finallyException caughtinside procBprocB's finallyinside procCprocC's finally*/}
}
在这个例子中,方法procA()通过抛出异常过早的跳出了try代码块,在退出之后执行finally子句。方法procB()通过return语句退出try代码块,在procB()方法返回前执行finally子句。在方法procC()中,try语句正常执行,没有错误,但是仍然会执行finally代码块。
请记住:
如果finally代码块和某个try代码块相关联,那么finally代码块会在这个try代码块结束后执行。
1.10 Java的内置异常
在标准包java.lang中,Java定义了一些异常类。在前面的例子中,已经用到了其中的几个。这些异常中最常用的是RuntimeException标准类型的子类。正如前面介绍的,在所有方法的throws列表中不需要包含这些异常。在Java语言中,这些异常被称为未经检查的异常,因为编译器不检查方法是否处理或抛出这些异常。表10-1列出了在java.lang中定义的未经检查的异常。表10-2列出了由java.lang包定义的其他异常,如果方法可能产生这些异常中的某个异常,并且方法本身不进行处理,那么必须在方法的throws列表中包含该异常,这些异常被称为经检查的异常。除了java.lang中的异常,Java还定义了与其他标准包相关联的异常。
表1-1 Java在java.lang中定义的未经检查的RuntimeException子类
异常 | 含义 |
---|---|
ArithmeticException | 算数错误,例如除零 |
ArrayIndexOutOfBoundsException | 数组索引越界 |
ArrayStoreException | 使用不兼容的类型为数组元素赋值 |
ClassCastException | 无效转换 |
EnumConstantNotPresentException | 试图使用未定义的枚举值 |
IllegalArgumentException | 使用非法参数调用 |
IllegalMonitorStateException | 非法的监视操作,例如等待未锁定的线程 |
IllegalStateException | 环境或应用程序处于不正确的状态 |
IllegalThreadStateException | 请求的操作与当前线程状态不兼容 |
IndexOutOfBoundsException | 某些类型的索引越界 |
NegativeArraySizeException | 使用负数长度创建索引 |
NullPointerException | 非法使用空引用 |
NumberFormatException | 字符串到数值格式的无效转换 |
SecurityException | 试图违反安全性 |
StringIndexOutOfBounds | 试图在字符串边界之外进行索引 |
TypeNotPresentException | 类型未找到 |
UnsupportedOperationException | 遇到不支持的操作 |
表1-2 Java在java.lang中定义的经检查的异常
异常 | 含义 |
---|---|
ClassNotFoundException | 类未找到 |
CloneNotSupportedException | 试图复制没有实现Cloneable接口的对象 |
IllegalAccessException | 对类的访问被拒绝 |
InstantiationException | 试图为抽象类或接口创建对象 |
InterruptedException | 一个线程被另一个线程中断 |
NoSuchFieldException | 请求的域变量不存在 |
NoSuchMethodException | 请求的方法不存在 |
ReflectiveOperationException | 与反射相关的异常的超类 |
1.11 创建自己的异常子类
尽管Java的内置异常处理了大部分常见错误,但是有时可能希望创建自己的异常类型,以处理特定于应用程序的情况。只需要定义Exception的子类(当然也是Throwable的子类)即可。您的子类不需要实际实现任何内容——只要他们存在于类型系统中就可以将他们用作异常。
Exception类没有为自己定义任何方法。当然,它继承了Throwable提供的方法。因此,所有异常,包括自己创建的那些异常,都可以获得Throwable定义的方法。表1-3列出了Throwable定义的方法。你可能希望在创建的异常类中重写这些方法中的一个或多个。
Exception类定义了4个公有构造函数,其中的两个支持链式异常,另外两个如下所示:
Exception()
Exception(String msg)
第一种形式创建没有描述的异常,第二种形式可以为异常指定描述信息。
虽然创建异常时指定描述通常是有用的,但是有时重写toString()方法更好一些。原因是:Throwable定义的toString()版本(Exception继承了该版本)首先显示异常的名称,之后跟一个冒号,然后显示您的描述。通过重写toString(),可以阻止显示异常名称和冒号。这样可以使输出变得更加清晰,在有些情况下您可能希望如此。
表1-3 Throwable定义的方法
方法 | 描述 |
---|---|
final void addSuppressed(Throwable exc) | 将exc添加到与调用异常关联的被抑制的异常列表中,主要用于新的带资源的try语句 |
Throwable fillInStackTrace() | 返回一个包含完整堆栈踪迹的Throwable对象,可以重新抛出该对象 |
Throwable getCause() | 返回引起当前异常的异常。如果不存在引起当前异常的异常,就返回null |
String getLocalizedMessage() | 返回异常的本地化描述 |
String getMessage | 返回异常的描述 |
StackTraceElement[] getStackTrace() | 返回一个包含堆栈踪迹的数组,数组元素的类型为StackTraceElement,每次一个元素。堆栈顶部的方法是抛出异常之前调用的最后一个方法,在数组的第一个元素中可以找到该方法。通过StackTraceElement类,程序可以访问与堆栈踪迹中每个元素相关的信息,例如方法名 |
final Throwable[] getSuppressed() | 获取与调用异常关联的被抑制的异常,并返回一个包含结果的数组。被抑制的异常主要由新的带资源的try语句生成 |
Throwable initCause(Throwable causeExec) | 将causeExec与调用异常关联到一起,作为调用异常的原因,返回对异常的引用 |
void printStackTrace() | 显示堆栈踪迹 |
void printStackTrace(PrintStream stream) | 将堆栈踪迹发送到指定的流中 |
void printStackTrace(PrintWriter stream) | 将堆栈踪迹发送到指定的流中 |
void setStackTrace(StackTraceElement[] elements) | 将elements中传递的元素设置为堆栈踪迹。该方法用于特殊的应用程序,在常规情况下不实用 |
String toString() | 返回包含异常描述的String对象,当通过println()输出Throwable对象时会调用该方法 |
下面的例子声明了一个新的Exception的子类,然后在一个方法中使用这个子类标识错误条件。该子类重写了toString()方法,从而可以仔细地修改显示的异常描述。
//This program creates a custom exception type.
public class MyException extends Exception{
private int detail;MyException(int a){
detail =a;}@Overridepublic String toString() {
return "MyException["+detail+"]";}
}
public class ExceptionDemo {
static void compute(int a) throws MyException {
System.out.println("Called compute(" + a + ")");if (a > 10)throw new MyException(a);System.out.println("Noraml exit");}public static void main(String[] args) {
try {
compute(1);compute(20);} catch (MyException e) {
System.out.println("Caught " + e);}}
}
这个示例定义了Exception的一个子类,名为MyException。这个子类相当简单:只有一个构造函数以及一个重写了的toString()方法,该方法显示异常的值。ExceptionDemo类定义了compute()方法,该方法抛出MyException对象。如果compute()方法的整型参数大于10,将会抛出异常。main()方法为MyException设置了一个异常处理程序,然后调用带有合法值(小于10)和非法值的compute()方法,以显示整个代码的这两条执行路径。下面是执行结果:
Called compute(1)
Noraml exit
Called compute(20)
Caught MyException[20]
1.12 链式异常
从JDK1.4开始,链式异常这一特性被包含进了异常子系统。通过链式异常,可以为异常关联另一个异常。第二个异常描述第一个异常的原因(描述当前异常原因的异常通常称为“引发异常”和“背后异常”)。例如,假设某个方法由于试图除零而抛出ArithmeticException异常,但是,导致问题发生的实际原因是发生了一个I/O错误,该错误导致为除数设置了不正确的值。尽管方法必须显示地抛出ArithmeticException异常,因为这是已经发生的错误,但是您可能还希望让调用代码知道背后的原因是I/O错误,使用链式异常可以处理这种情况以及所有其他存在多层异常的情况。
为了使用链式异常,向Throwable类添加了两个构造函数和两个方法。这两个构造函数如下所示:
Throwable(Throwable causeExc)
Throwable(String msg,Throwable causeExc)
在第一种形式中,causeExc是引发当前异常的异常,即causeExc是引发当前异常的背后原因。第二种形式允许在指定引发异常的同时指定异常描述。这两个构造函数也被添加到了Error、Exception以及RuntimeException类中。
添加到Throwable类中的链式异常方法是getCause()和initCause()。在表1-3中列出了这些方法:
Throwable getCause()
Throwable initCause(Throwable causeExc)
getCause()方法返回引发当前异常的异常。如果不存在背后异常,就返回null。initCause()方法将causeExc和调用异常关联到一起,并返回异常的引用。因此,可以在创建异常之后将异常和背后异常关联到一起。但是,背后异常只能设置一次。因此,对于每个异常对象只能调用initCause()方法一次。此外,如果通过构造函数设置了引发异常,那么就不能再使用initCause()方法进行设置。通常,initCause()方法用于为不支持前面描述的两个附加构造函数的遗留异常类设置问题。
下面的例子演示了链式异常处理机制:
//Demonstrate exception chaining.
public class ChainExcDemo {
static void demoproc() {
//create an exceptionNullPointerException e = new NullPointerException("top layer");//add a causee.initCause(new ArithmeticException("cause"));throw e;}public static void main(String[] args) {
try {
demoproc();} catch (NullPointerException e) {
//display top level exceptionSystem.out.println("Caught: " + e);//display cause exceptionSystem.out.println("Original cause: " + e.getCause());}/*输出结果:Caught: java.lang.NullPointerException: top layerOriginal cause: java.lang.ArithmeticException: cause*/}
}
在这例子中,顶级异常是NullPointerException,并为该异常添加了一个引发异常ArithmeticException。当从demoproc()方法抛出异常时,异常由main()方法捕获。在此,显示顶级异常,然后显示背后异常,背后异常是通过调用getCause()方法得到的。
链式异常可以包含所需要的任意深度。因此,引发异常本身可能还包含引发异常。但是应当清楚,过长的链式异常可能是一种不良的设计。
并不是所有程序都需要链式异常。但是,对于那些需要使用背后原因信息的情况,链式异常是完美的解决方案。
1.13 从JDK7开始添加的3个异常特性
从JDK7开始,异常系统添加了3个有趣并且有用的特性。第1个特性是:当资源(例如文件)不再需要时能够自行释放。该特性的基础是try语句的扩展形式,称为带资源的try语句。第2个新特性称为多重捕获(multi-catch),第3个特性有时被称为最后重新抛出(final rethrow)或更精确的重新抛出(more precise rethrow)。下面介绍这两个特性。
多重捕获特性允许通过相同的catch子句捕获两个或更多个异常。两个或更多个异常处理程序使用相同的代码序列并非不寻常,尽管它们针对不同的异常进行响应。现在不必逐个捕获所有异常,可以通过一条catch子句使用相同的代码处理所有异常。
为了使用多重捕获,在catch子句中使用或运算符(|)分隔每个异常。每个多重捕获参数都被隐式地声明为final(如果愿意的话,也可以显示指定final,但这不是必须的)。因此,不能为他们赋予新值。
下面的catch语句使用多重捕获特性同时捕获ArithmeticException和ArrayIndexOutOfBoundsException:
catch(ArithmeticException | ArrayIndexOutOfBoundsException)
下面的程序显示了多重捕获特性的实际应用:
//Demonstrate the multi-catch feature.
public class MutilCatch {
public static void main(String[] args) {
int a = 10, b = 0;int vals[] = {
1, 2, 3};try {
int result = a / b;//generate an ArithmeticException//vals[10] = 9;//generate an ArrayIndexOutOfBoundsException//This catch clause catches both exceptions.} catch (ArithmeticException | ArrayIndexOutOfBoundsException e) {
System.out.println("Exception caught: " + e);}System.out.println("After mutil-catch.");}
}
当试图除零时,程序会产生ArithmeticException异常。如果注释掉除法语句,并移除下一行的注释符号,将会产生ArrayIndexOutOfBoundsException异常。这两个异常是由同一条catch语句捕获的。
“更精确地重新抛出”异常特性会对重新抛出的异常进行限制,只能重新抛出满足以下条件的经检查的异常:由关联的try代码块抛出,没有被前面的catch子句处理过,并且是参数的子类类型或超类型。虽然这个功能不经常需要,但是确实提供了这一特性。为了强制使用“更精确地重新抛出”异常特性,catch参数必须有效地或显示地被声明为final,这意味着catch代码块中不能为之赋新值。
1.14 使用异常
异常处理为控制复杂程序提供了一种强大的机制,具有许多动态的运行时特征。考虑将try、throw以及catch作为异常处理程序逻辑中的错误和不寻常边界条件的清晰方式是很重要的。不同于某些其他语言,通过返回错误代码表示操作失败,Java使用异常表示操作失败。因此,如果某个方法可能会失败,应当使其抛出异常。这是处理失败模式时更为清晰的方法。
最后一点:不应当将Java的异常处理语句作为进行非本地分支的一般机制。如果您这么做的话,会让代码变得混乱并难以维护。