1.4.4 Java 7的异常处理新特性
下面详细介绍Java 7中引入的与异常处理相关的新特性。
1.一个catch子句捕获多个异常
在Java 7之前的异常处理语法中,一个catch子句只能捕获一类异常。在要处理的异常种类很多时这种限制会很麻烦。每一种异常都需要添加一个catch子句,而且这些catch子句中的处理逻辑可能都是相同的,从而会造成代码重复。虽然可以在catch子句中通过这些异常的基类来捕获所有的异常,比如使用Exception作为捕获的类型,但是这要求对这些不同的异常所做的处理是相同的。另外也可能会捕获到某些不应该被捕获的非受检异常。而在某些情况下,代码重复是不可避免的。比如某个方法可能抛出4种不同的异常,其中有2种异常使用相同的处理方式,另外2种异常的处理方式也相同,但是不同于前面的2种异常。这势必会在catch子句中包含重复的代码。
对于这种情况,Java 7改进了catch子句的语法,允许在其中指定多种异常,每个异常类型之间使用“|”来分隔,如代码清单1-12所示。ExceptionThrower类的manyExceptions方法会抛出ExceptionA、ExceptionB和ExceptionC三种异常,其中对ExceptionA和ExceptionB采用一种处理方式,对ExceptionC采用另外一种处理方式。
代码清单1-12 在catch子句中指定多种异常
public class ExceptionHandler{
public void handle(){
ExceptionThrower thrower=new ExceptionThrower();
try{
thrower.manyExceptions();
}catch(ExceptionA|ExceptionB ab){
}catch(ExceptionC c){
}
}
}
这种新的处理方式使上面提出的问题得到了很好的解决。需要注意的是,在catch子句中声明捕获的这些异常类中,不能出现重复的类型,也不允许其中的某个异常是另外一个异常的子类,否则会出现编译错误。如果在catch子句中声明了多个异常类,那么异常参数的具体类型是所有这些异常类型的最小上界。
关于一个catch子句中的异常类型不能出现其中一个是另外一个的子类的情况,实际上涉及捕获多个异常的内部实现方式。比如在代码清单1-13中,虽然NumberFormat-Exception是RuntimeException的子类,但是这段代码是可以通过编译的。
代码清单1-13 catch子句中声明异常的顺序的正确示例
public void testSequence(){
try{
Integer.parseInt("Hello");
}
catch(NumberFormatException|RuntimeException e){}
}
但是如果把catch子句中两个异常的声明位置调换一下,就会出现编译错误。代码清单1-14会产生编译错误。
代码清单1-14 catch子句中声明异常的顺序的错误示例
public void testSequenceError(){
try{
Integer.parseInt("Hello");
}
catch(RuntimeException|NumberFormatException e){}
}
原因在于,编译器的做法其实是把捕获多个异常的catch子句转换成了多个catch子句,在每个catch子句中捕获一个异常。代码清单1-14中的testSequenceError方法实际上相当于代码清单1-15。这段代码显然是不能通过编译的,因为在上一个catch子句中已经捕获了RuntimeException,在下一个catch子句中无法再捕获其子类异常。
代码清单1-15 代码清单1-14中异常捕获的等价形式
public void testSequenceError(){
try{
Integer.parseInt("Hello");
}
catch(RuntimeException e){}
catch(NumberFormatException e){}
}
关于catch子句中异常参数的具体类型,可以参看代码清单1-16。这里catch子句的异常类型包括ExceptionASub1和ExceptionASub2,因此参数“e”的具体类型是ExceptionASub1和ExceptionASub2在类继承层次结构上的最小祖先类,即ExceptionA,在catch子句中可以调用ExceptionA中的方法。因为所有的异常都是Exception类的后代,所以这样一个最小的上界总是会存在的。
代码清单1-16 catch子句中异常参数的具体类型
public void testCatchType(){
try{
throwException();
}
catch(ExceptionASub1|ExceptionASub2 e){
e.methodInExceptionA();
}
}
2.更加精确的异常抛出
在进行异常处理的时候,如果遇到当前代码无法处理的异常,应该把异常重新抛出,交由调用栈的上层代码来处理。在重新抛出异常的时候,需要判断异常的类型。Java 7对重新抛出异常时的异常类型做了更加精确的判断,以保证抛出的异常的确是可以被抛出的。这个改进初看起来会让人有点费解,因为从语义上来说,不能被抛出来的异常是不会被重新抛出的。但是在Java 7之前,Java编译器并不能做出精确的判断,因此会存在一些隐含的不正确的情况。在Java 7中,如果一个catch子句的异常类型参数在catch代码块中没有被修改,而这个异常又被重新抛出,编译器会知道这个重新被抛出的异常肯定是try语句块中可以抛出的异常,同时也是没有被之前的catch子句捕获的异常。代码清单1-17给出了一个精确的异常抛出的例子来说明Java 7之前的编译器和Java 7编译器不一样的行为。
代码清单1-17 精确的异常抛出的示例
public class PreciseThrowUse{
public void testThrow()throws ExceptionA{
try{
throw new ExceptionASub2();
}
catch(ExceptionA e){
try{
throw e;
}
catch(ExceptionASub1 e2){//编译错误
}
}
}
}
在上面的代码中,异常类ExceptionASub1和ExceptionASub2都是ExceptionA的子类,而且这两者之间并没有继承关系。方法testThrow中首先抛出了ExceptionASub2异常,通过第一个catch子句捕获之后重新抛出。在这里,Java编译器可以准确知道变量e表示的异常类型是ExceptionASub2,接下来的第二个catch子句试图捕获ExceptionASub1类型的异常,这显然是不可能的,因此会产生编译错误。上面的代码在Java 6编译器上是可以通过编译的。对于Java 6编译器来说,第二个try子句中抛出的异常类型是前一个catch子句中声明的ExceptionA类型,因此在第二个catch子句中尝试捕获ExceptionA的子类型ExceptionASub1是合法的。