Java中的基本try-catch-finally异常处理
本文总结了try-catch-finally子句错误处理工作原理的基础。这些示例使用Java,但是C#的规则相同。 Java和Cexceptions之间的唯一区别是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"语句之后立即停止执行。 " throw"语句之后的任何语句都不会执行。在上面的示例中,"返回numberToDivide / numberToDivideBy;"如果抛出BadNumberException,则不会执行该语句。当异常被" 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"); }
捕获子句中的BadNumberException参数e指向从除法(如果抛出异常)抛出的异常。
如果在try块内执行的任何调用方法或者执行的语句均未引发任何误解,则将catch块简单地忽略。它不会被执行。
如果在try块中引发了异常(例如,除法方法中的异常),则调用方法callDivide的程序流将被中断,就像除法中的程序流一样。程序流在可以捕获引发的异常的调用堆栈中的catch块处恢复。在上面的示例中," System.out.println(result);"如果从除法中抛出异常,将不会执行该语句。取而代之的是,程序执行将在" catch(BadNumberException e){}"块内恢复。
如果在catch块中引发了异常并且未捕获到该异常,则catch块将被中断,就像try块一样。
当catch块完成时,程序将继续执行catch块之后的所有语句。在上面的示例中," System.out.println("除法尝试已完成");"语句将始终被执行。
传播异常
我们不必捕获其他方法引发的异常。如果我们无法对调用该方法的异常进行任何处理,则可以让该方法将异常沿调用堆栈传播到调用此方法的方法。如果这样做,则调用引发异常的方法的方法也必须声明引发异常。在这种情况下,callDivide()方法的外观如下。
public void callDivide() throws BadNumberException{ int result = divide(2,1); System.out.println(result); }
注意try-catch块是如何消失的,并且callDivide方法现在声明它可以引发BadNumberException。如果除法方法抛出异常,程序执行仍会中断。因此," System.out.println(result);"如果除法方法抛出异常,则该方法将不会执行。但是现在程序的执行不会在callDivide方法内恢复。异常传播到调用callDivide的方法。直到调用堆栈中某个地方的catch块捕获到异常,程序才会恢复执行。在引发异常的方法与捕获异常的方法之间的调用堆栈中,所有方法的执行都将在代码中引发或者传播异常的点处停止执行。
示例:捕获IOException
如果在try-catch块内的一系列语句中引发异常,则该语句序列将中断,控制流将直接跳到catch块。此代码可能会在多个地方被异常中断:
public void openFile(){ try { // constructor Jan throw FileNotFoundException FileReader reader = new FileReader("someFile"); int i=0; while(i != -1){ //reader.read() Jan 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()方法调用引发IOException,则以下System.out.println((char)i);不执行。最后一个reader.close()或者System.out.println("-File End ---")都不是;陈述。相反,程序直接跳到catch(IOException e){...} catch子句。如果新的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虚拟机捕获了该异常为止。
最后
我们可以在try-catch块上添加最后一个子句。即使在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块中引发了异常,但没有捕获到该异常,则该try块和catch-block一样会中断该finally块。这就是为什么前面的示例在try-catch块中包装的finally块中调用了reader.close()方法的原因:
} 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 ---");方法调用将始终执行。如果没有抛出未经检查的异常。在后面的章节中,将进一步介绍有关已选中和未选中的内容。
我们既不需要接球,也不需要finally挡块。我们可以使用try块使它们之一或者全部具有它们,但不能全都没有。这段代码没有捕获异常,但可以将其向上传播到调用堆栈中。由于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子句中关闭数据库连接。最终如何处理异常还取决于我们为应用程序选择已检查还是未检查的异常。在其他文本中,有关错误处理的更多信息。