由于硬件错误、资源耗尽以及输入错误的数据,都可能导致Java应用运行时产生异常。在Java中异常处理是围绕使用try-catch-finally语句块、throws、throw这几个关键词来处理,这无需多解释。如果异常没有被捕捉那么异常将沿着方法的调用栈一直向上传播,如果传播的过程中一直没有catch语句块捕捉,则最终传播到main方法,最后从main方法抛出,有JRE来处理。
Java的异常层次结构
Java类库中有一个java.lang.Throwable类,其继承自java.lang.Object类,是所有异常类的父类。如下图:
checked和unchecked异常
Java的异常分为checked exception和unchecked exception。其中java.lang.RuntimeExcetion和java.lang.Error是unchecked exception,其他都是checked exception。checked exception和unchecked exception在作用上并没有差别,唯一的差别就在于使用checked exception时的合法性是在编译时刻由编译器来检查,这类异常必须处理,否则编译不通过。而Error类一般代表由硬件导致的异常,RuntimeException是在运行时抛出的异常,编译器是检查不出来的。例如:
package net.oseye; import java.net.ServerSocket; public class ExceptionTest { public static void main(String[] args) { ServerSocket ssocket=new ServerSocket(8899); } }
是不能编译通过的,因为ServerSocket类在构造器被调用后,有可能抛出java.io.IOException异常,而这个异常是checked exception,必须捕捉:
package net.oseye; import java.io.IOException; import java.net.ServerSocket; public class ExceptionTest { public static void main(String[] args) { try { ServerSocket ssocket=new ServerSocket(8899); } catch (IOException e) { e.printStackTrace(); } } }
再次抛出异常
根据程序流程以及输入自定义抛出异常或者自己不处理异常而是上报给上级处理,都可能用到再次抛出的做法。一旦方法可能抛出checked异常,则在方法声明时必须特别指出,否则编译不过。声明语法如下:
访问限制 [static] 返回值类型 方法名(参数列表) throws 异常{ 方法体 }
其中throws可以声明可能抛出的多个异常,其中以","分割。方法体中可能抛出的异常类型必须和声明中异常的类型相同或其子类。
再次抛出异常可以是显示抛出如:
package net.oseye; import java.io.IOException; import java.net.ServerSocket; public class ExceptionTest { public static void main(String[] args) throws IOException { try { ServerSocket ssocket=new ServerSocket(8899); } catch (IOException e) { throw e; } } }
也可以隐形抛出如:
package net.oseye; import java.io.IOException; import java.net.ServerSocket; public class ExceptionTest { public static void main(String[] args) throws IOException { ServerSocket ssocket=new ServerSocket(8899); } }
声明抛出异常也是方法声明的组成部分,所以在重写方法时对异常的抛出是由限制的。重写后的方法一定不能声明抛出新的异常或比原方法范畴更广的异常。
自定义异常
开发人员一般花费大量的精力对程序的主要功能部分进行设计,而忽略了对异常的设计,但这会对应用的整体架构造成影响,所有有必须要对异常进行精心设计。
一直以来,关于在程序中使用checked异常还是unchecked异常,开发者一直存在争议。checked异常的好处就是利用编译器的检查来避免开发者忽略了没处理的异常,但缺点也是显而易见的:造成代码冗余,程序中有很多没有实际作用的代码;而unchecked异常的好处是代码整洁、干净,但缺点是不能防患于未然。
目前的主流建议是最好优先使用unchecked异常。所以使用自定义异常时至少需要一个继承自RuntimeException的异常类,如果还需要使用checked异常,还要有另外一个继承自Exception的异常类。如果程序中可能出现的异常比较多,应该设计好自己的异常层次结构,使用不同层的开发者只关注自己应该关心的异常。
在Java 7中对异常做了三个重要的改动
- 支持在一个catch字句中同时捕获多个异常
每个异常类型之间使用“|”来分割如:package net.oseye; public class ExceptionTest { public static void main(String[] args){ try{ .... }catch(ExceptionA | ExceptionB ab){ .... }catch(ExceptionC c){ .... } } }
- ab的类型是ExceptionA和ExceptionB的共同超类类型;
- catch(ExceptionA | ExceptionB ab)是编译器处理的和catch(ExceptionA ab){....}catch(ExceptionB ab){....}等价,所以也要注意顺序问题;
- 再次抛出异常事件的异常类型更加精确
package net.oseye; public class ExceptionTest { public static void main(String[] args) throws ExceptionA{ try{ throw new ExceptionAsub1(); }catch(ExceptionA e){ throw e; }catch(ExceptionAsub2 e){ //编译错误 .... } } }
- Throwable 增加了 addSuppressed方法,辅助处理丢失异常
在try-catch-finally语句块中,如果在catch中捕捉了异常,但不处理,而是通过方法调用栈抛给上层,但在finally中又抛出了异常,那么catch中的异常将丢失。
一般有两种处理方式:忽略finally的异常,因为根源是try语句的异常;记录try语句块的异常在finally一起抛出。
我们来看第一种处理方式可以这样:public class ExceptionTest { public static void read(String filename) throws BaseException{ FileInputStream input = null; IOException readException = null; try{ input =new FileInputSteam(filename); }catch(IOException e){ readException = e; }finally{ if(input != null){ try{ input.close(); }catch(IOException e){ if(readException == null){ readException = e; } } } if(readException != null){ throw new BaseException(readException); } } } }
public class ExceptionTest { public static void read(String filename) throws IOException{ FileInputStream input = null; IOException readException = null; try{ input =new FileInputSteam(filename); }catch(IOException e){ readException = e; }finally{ if(input != null){ try{ input.close(); }catch(IOException e){ if(readException != null){ readException.addSuppressed(e); } } } if(readException != null){ throw readException; } } } }