原文链接 作者:Jakob Jenkov 译者:fangqiang08(fangqiang08@gmail.com)
这一小节概述了try-catch-finally 语句是怎样处理错误的,文中的例子是Java的,但是同样的规则也适用于C#。java和C#中异常的唯一区别就是C#中没有已检查异常。已检查异常和未检查异常将在后面小节更加详细地介绍。
程序中的异常表明一些错误或者异常情况发生了,异常如果没有被处理,继续程序流程是没有意义的。一个方法可能会因为各种原因抛出异常,比如输入参数是无效的(当期待获得正数时输入了负数等。)
调用栈
下面介绍调用栈。调用栈是指从当前的方法开始一直到main方法处的方法调用序列。如果方法A调用方法B,方法B调用方法C,则调用栈如下所示:
A
B
C
当方法C返回后,调用栈仅仅包含A和B,如果方法B再调用方法D,则调用栈如下所示:
A
B
D
理解调用栈对理解异常传播是非常重要的。从抛出异常的方法开始,异常根据调用栈传播,直到调用栈中某个方法捕获该异常。更多细节稍后讨论。
抛出异常
如果一个方法要抛出异常,那么要在方法签名中声明这个异常,然后在方法里包含一条throw语句。下面是一个例子:
public void divide(int numberToDivide, int numberToDivideBy)
throws BadNumberException{
if(numberToDivideBy == 0){
throw new BadNumberException("Cannot divide by 0");
}
return numberToDivide / numberToDivideBy;
}
当一个异常被抛出时,方法在抛出异常处停止运行。throw语句后的任何语句都将不会被执行了。在上面这个例子中,如果抛出BadNumberException异常,那么”return numberToDivide / numberToDivideBy;”这条语句将不会被执行。当异常被catch语句块捕获时,程序才会继续执行。捕获异常稍后解释。
只要在方法签名中做了声明,你可以抛出任何异常。你也可以创建自己的异常。异常是继承自java.lang.Exception的标准java类或者任何其他的内置异常类。如果一个方法声明抛出异常A,那么方法抛出异常A的子类异常也是合法的。
捕获异常
如果一个方法调用了另一个会抛出已检查异常的方法,那么这个调用方法必须要么传递这个异常,要么捕获这个异常。捕获异常是利用try-catch语句块来实现的。下面是一个例子:
public void callDivide(){
try {
int result = divide(2,1);
System.out.println(result);
} catch (BadNumberException e) {
//do something clever with the exception
System.out.println(e.getMessage());
}
System.out.println("Division attempt done");
}
当抛出异常时,catch语句中的BadNumberException 类型参数e就代表从divide方法中抛出的异常。
如果try语句块中任何被调用的方法和执行的语句都没有抛出异常,catch语句块仅仅被忽略,不会被执行。
如果try语句块中抛出异常,例如在divide方法中抛出异常,调用方法(即callDrive方法)中的程序流将会像divide方法中的程序流一样被中断。程序流将在调用栈中某个能够捕获这个异常的catch语句块处恢复。上面例子中,如果一个异常从divide方法中抛出,那么“System.out.println(result);” 语句将不会被执行。这时程序将会在catch (BadNumberException e) { }语句块里恢复执行。
如果在ctach语句块里抛出了一个没有被捕获的异常,那么catch语句块的运行将会像try语句中块抛出异常一样被中断。
当catch语句块执行完,程序将会执行catch语句块后面的所有语句。上面例子中的”System.out.println(“Division attempt done”);” 语句将永远会被执行。
异常传播
你也没有必要捕获其他方法中抛出的异常。如果你在方法抛出异常的地方不能对异常做任何事,你可以让方法根据调用栈将这个异常传播到调用这个方法的其他方法处。如果你这样做,调用这个将抛出异常方法的其他方法必须要在方法签名中声明抛出这个异常。下面是callDivide()方法在这种情况下的例子:
public void callDivide() throws BadNumberException{
int result = divide(2,1);
System.out.println(result);
}
注意try-catch语句块不见了,callDivide 方法声明它会抛出一个BadNumberException异常。如果divide方法里抛出一个异常,程序仍将被中断。因此如果divide方法里抛出一个异常,”System.out.println(result);” 语句将不会被执行。但是这样的话callDivide 方法中(因为抛出异常而中断的的)程序执行不会被恢复。异常会被传播到调用callDivide的方法中。直到调用栈中的某个catch语句块捕获了这个异常,程序执行才会恢复。根据调用栈,从抛出异常的方法到捕获异常的方法之间的所有方法的运行都将停止在异常抛出或者传播的代码处。
捕获IOException示例
如果一个异常在try语句块中被抛出,程序的顺序执行将会被中断,控制流将直接跳转到catch语句块。代码将会因为异常在几个地方被中断:
public void openFile(){
try {
// constructor may throw FileNotFoundException
FileReader reader = new FileReader("someFile");
int i=0;
while(i != -1){
//reader.read() may throw IOException
i = reader.read();
System.out.println((char) i );
}
reader.close();
System.out.println("--- File End ---");
} catch (FileNotFoundException e) {
//do something clever with the exception
} catch (IOException e) {
//do something clever with the exception
}
}
如果reader.read()方法调用抛出一个IO异常,下面的System.out.println((char) i );语句不会被执行。最后的reader.close() 和System.out.println(“— File End —“);语句也不会被执行。程序直接调转到catch(IOException e){ … }语句块处。如果new FileReader(“someFile”); 构造器中调用抛出一个异常,那么try语句块中的代码都不会被执行了。
传播IOException示例
下面的代码仍然以上面的方法为例,不过这里没有捕获异常:
public void openFile() throws IOException {
FileReader reader = new FileReader("someFile");
int i=0;
while(i != -1){
i = reader.read();
System.out.println((char) i );
}
reader.close();
System.out.println("--- File End ---");
}
如果reader.read() 方法抛出一个异常,程序将会停止运行,异常根据调用栈传播到调用openFile()方法的方法处。如果这个调用方法有try-catch语句块,异常将在这里被捕获。如果这个调用方法也只是抛出异常,这个调用方法的运行将在调用openFile()方法处中断,异常根据调用栈往外传播。异常就是这样根据调用栈往外传播,直到某个方法或者java虚拟机捕获了这个异常。
Finally
你也可以在try-catch语句块后增加一个finally语句块。不论try还是catch语句块中抛出异常,finally语句块中的代码将永远执行。如果代码里的try或者catch语句块中有个return语句,finally语句块中的代码将会在方法返回之前执行。finally示例见下:
public void openFile(){
FileReader reader = null;
try {
reader = new FileReader("someFile");
int i=0;
while(i != -1){
i = reader.read();
System.out.println((char) i );
}
} catch (IOException e) {
//do something clever with the exception
} finally {
if(reader != null){
try {
reader.close();
} catch (IOException e) {
//do something clever with the exception
}
}
System.out.println("--- File End ---");
}
}
不论try或者catch语句块中是否有异常抛出,finally语句块中的代码都将被执行。这个例子表明不论try、catch语句块中的程序运行情况如何,文件读取始终会被关闭。
注意:如果finally语句块中抛出一个异常,并且没有被捕获,finally语句块就像try、catch语句块抛出异常一样也会被中断运行。这就是为什么上面的例子中finally语句块中的reader.close() 方法也被try-catch语句块包裹原因:
} finally {
if(reader != null){
try {
reader.close();
} catch (IOException e) {
//do something clever with the exception
}
}
System.out.println("--- File End ---");
}
通过这样,System.out.println(“— File End —“);语句将会永远被执行。更准确地说是没有未检查异常被抛出时是这样。更多关于已检查异常和未检测异常的知识将在后面章节介绍。
你的程序中不需要同时又catch、finally语句块。try语句块后可以仅仅有catch语句块或者finally语句块,但是try语句块后既没有catch语句块也没有finally语句块是不行的。下面的代码不会捕获异常而是让异常根据调用栈往外传播,但是由于finally语句块的存在,即是有异常抛出,程序仍旧能关闭打开的文件。
public void openFile() throws IOException {
FileReader reader = null;
try {
reader = new FileReader("someFile");
int i=0;
while(i != -1){
i = reader.read();
System.out.println((char) i );
}
} finally {
if(reader != null){
try {
reader.close();
} catch (IOException e) {
//do something clever with the exception
}
}
System.out.println("--- File End ---");
}
}
注意上面catch语句块不见了。
捕获异常还是传播异常?
你可能疑问:程序中抛出的异常是应该捕获还是让其传播?这取决于具体情况。在许多应用程序中,除了告诉用户请求操作失败,你不能对异常做其他更多事情。在这种情况下,你可以在调用栈中的第一个方法里捕获所有的、或者大多数的异常。在传播异常时,你仍然可以处理异常(通过finally语句)。比如,在一个web应用程序中的数据库连接出现了错误,即使你所能做的只是告诉用户这个操作失败,你仍然应该在finally语句中关闭这个数据库连接。最终如何处理异常也取决于你的程序中抛出的是已检查异常还是未检查异常。关于已检查异常、未检查异常,后面将有更多文章介绍。