1.4.3 处理异常
处理异常的基本思路也比较简单。一般来说就两种选择:处理或是不处理。如果某个异常在当前的调用栈层次上是可以处理和应该处理的,那么就应该直接处理掉;如果不能处理,或者不适合在这个层次上处理,就可以选择不理会该异常,而让它自行往更上层的调用栈上传递。如果当前的代码位于抽象层次的边界,就需要首先捕获该异常,重新包装之后,再往上传递。
决定是否在某个方法中处理一个异常需要判断从异常中恢复的方式是否合理。比如一个方法要从文件中读取配置信息,进行文件操作时可能抛出IOException。当出现异常的时候,如果可以采取的恢复措施是使用默认值,那么在这个方法中处理IOException就是合理的。而在同样的场景中,如果某些配置项没有合法的默认值,必须要手工设置一个值,那么读取文件时出现的IOException就不应该在这个方法中处理。
在确定了需要对异常进行处理之后,按照程序本身的逻辑来处理即可。下面将要介绍的是一个处理异常时容易忽略的问题—消失的异常。
开发人员对异常处理的try-catch-finally语句块都比较熟悉。如果在try语句块中抛出了异常,在控制权转移到调用栈上一层代码之前,finally语句块中的语句也会执行。但是finally语句块在执行的过程中,也可能会抛出异常。如果finally语句块也抛出了异常,那么这个异常会往上传递,而之前try语句块中的那个异常就丢失了。代码清单1-9给出了一个示例,try语句块会抛出NumberFormatException,而在finally语句块中会抛出ArithmeticException。对这个方法的使用者来说,他最终看到的只是finally语句块中抛出的ArithmeticException,而try语句中抛出的NumberFormatException消失不见了。
代码清单1-9 异常消失的示例
public class DisappearedException{
public void show()throws BaseException{
try{
Integer.parseInt("Hello");}
catch(NumberFormatException nfe){
throw new BaseException(nfe);}finally{
try{
int result=2/0;
}catch(ArithmeticException ae){
throw new BaseException(ae);}
}}
}
其实这样的例子在日常开发中也是比较常见的。比如在打开一个文件进行读取的时候,肯定需要用try-catch语句块来捕获其中的IOException,并且在finally语句块中关闭文件输入流。在关闭输入流的时候可能会抛出异常,造成之前在读取文件时产生的异常丢失。还有一个典型的情况发生在数据库操作的时候,在finally语句块中关闭数据库连接。由于之前产生的异常丢失,开发人员可能无法准确定位异常的发生位置,造成错误的判断。
对这种问题的解决办法一般有两种,一种是抛出try语句块中产生的原始异常,忽略在finally语句块中产生的异常。这么做的出发点是try语句块中的异常才是问题的根源。另外一种是把产生的异常都记录下来。这么做的好处是不会丢失任何异常。在Java 7之前,这种做法需要实现自己的异常类,而在Java 7中,已经对Throwable类进行了修改以支持这种情况。
第一种做法的实现方式如代码清单1-10所示。
代码清单1-10 抛出try语句块中产生的原始异常的示例
public class ReadFile{
public void read(String filename)throws BaseException{
FileInputStream input=null;
IOException readException=null;
try{
input=new FileInputStream(filename);
}catch(IOException ex){
readException=ex;
}finally{
if(input!=null){
try{
input.close();
}catch(IOException ex){
if(readException==null){
readException=ex;
}
}
}
if(readException!=null){
throw new BaseException(readException);
}
}
}
}
第二种做法需要利用Java 7中为Throwable类增加的addSuppressed方法。当一个异常被抛出的时候,可能有其他异常因为该异常而被抑制住,从而无法正常抛出。这时可以通过addSuppressed方法把这些被抑制的方法记录下来。被抑制的异常会出现在抛出的异常的堆栈信息中,也可以通过getSuppressed方法来获取这些异常。这样做的好处是不会丢失任何异常,方便开发人员进行调试。代码清单1-11给出了使用addSuppressed方法记录异常的示例。
代码清单1-11 使用addSuppressed方法记录异常的示例
public class ReadFile{
public void read(String filename)throws IOException{
FileInputStream input=null;
IOException readException=null;
try{
input=new FileInputStream(filename);
}catch(IOException ex){
readException=ex;
}finally{
if(input!=null){
try{
input.close();
}catch(IOException ex){
if(readException!=null){
readException.addSuppressed(ex);
}
else{
readException=ex;
}
}
}
if(readException!=null){
throw readException;
}
}
}
}
这种做法的关键在于把finally语句中产生的异常通过addSuppressed方法加到try语句产生的异常中。