简介
异常处理是对可能出现的异常进行处理,以防止程序遇到异常时被卡死,处于一直等待,或死循环。
异常有两个过程,一个是抛出异常;一个是捕捉异常。
java语言可以说是提供了过于完善的异常处理机制,其包括Error和Exception两个部分。他们都继承自共同的基类Throwable。
Error属于JVM运行中发生的一些错误,虽然并不属于开发人员的范畴,但是有些Error还是由代码引起的,比如StackOverflowError经常由递归操作引起,一般无法挽救,只能靠JVM。
Exception分为检查类型(checked)和未检查类型(unchecked)。检查类型的异常:程序员明确的去声明或者用try..catch语句来处理的异常,而非检查类型的异常则没有这些限制,比如我们常见的 NullPointerException 就是非检查类型的,他继承自RuntimeException。
java是目前主流编程语言中唯一一个推崇使用检查类型异常的,至少sun是这样的。关于使用checked还是unchecked异常的论战一直很激烈。下面是一张java语言中异常的类关系图。
基本使用
我们在使用java的文件或者数据库操作时接触的一些异常:比如IOException、SQLException等,这些方法被声明可能会抛出某种异常,我们需要对其进行捕获处理,这就需要基本的try..catch语句了。下图就是我们经常写的一个基本结构。try语句块中写”可能会抛出异常的代码“,catch语句块对其进行捕获。我们看到catch的参数写的是一个Exception对象,这就意味着这个语句块可以捕获所有的检查类型的异常(虽然这并不是一种好的写法),finally总是会保证在最后执行,一般我们在里面处理一些清理的工作,比如关闭文件流或者数据库,网络等操作。
当然上面的语句块结构是灵活的,但try是必须有的,catch和finally至少有一个,当然catch的数量可以有多个。有时try语句块中可能抛出多种类型的异常,这个时候,我们可以写多个catch语句来捕获不同类型的异常,一个比较好的写法如下:
try{ // ..invoke some methods that may throw exceptions }catch(ExceptionType1 e){ //...handle exception }catch(ExceptionType2 e){ //...handle exception }catch(Exception e){ //...handle exception }finally{ //..do some cleaning :close the file db etc. }
当异常不满足前两个type的时候,exception会将异常捕获。我们发现这个写法比较类似switch case的结构控制语句,一旦某个catch得到匹配后,其他的就不会就匹配了,有点像加了break的case。有一点需要注意catch(Exception)一定要写在最后面,catch是顺序匹配的,后面匹配Exception的子类,编译器就会报错。
try语句实际上执行的时候会导致栈操作。即要保存整个方法的调用路径,这势必会使得程序变慢。fillInStackTrace()是Throwable的一个方法,用来执行栈的操作,他是线程同步的,本身也很耗时。这里问题在StackOverFlow上曾经有过一段非常经典的讨论,原文。 的确当我们在try中什么都不做,或者只执行一个类似加法的简单调用,那么其执行效率和goto这样的控制语句是几乎一样的。但是谁会写这样的代码呢?
总之不要总是试图通过try catch来控制程序的结构,无论从效率还是代码的可读性上都不好。
try catch好的一面
try catch虽不推荐用于程序结构的控制,但也具有重要的意义,其设计的一个好处就是,开发人员可以把一件事情当做事务来处理,事务也是数据库中重要的概念,举个例子,比如完成订单的这个事务,其中包括了一个动作序列,包括用户提交订单,商品出库,关联等。当这个序列中某一个动作执行失败的时候,数据统一恢复到一个正常的点,这样就不会出现你付完了帐商品却没有给你的情况。我们在try语句块中就像执行一个事务一样,当出现了异常,就会在catch中得到统一的处理,保证数据的完整无损。其实很多不好的代码也是因为没有好好利用catch语句的语言,导致很多异常就被淹没了。
定制详细的异常
我们可以自己定义异常,以捕获处理某个具体的例子。创建自己的异常类,可以直接继承Exception或者RuntimeException。区别是前者是简称类型的,而后者为检查类型异常。Sun官方力挺传统的观点,他建议开发者都是用检查类型的异常,即你一定要去处理的异常。下面是定义的一个简单的异常类.
public class SimpleException extends Exception{ SimpleException(){} SimpleException(String info){ super(info); } }
我们覆写了两个构造方法,这是有意义的。通过传递字符串参数,我们创建一个异常对象的时候,可以记录下详细的信息,这样这个异常被捕获的时候就会显示我们之前定义的详细信息。比如用下面的代码测试一下我们定义的异常类:
public class Test { public void fun() throws SimpleException{ throw new SimpleException("throwing from fun"); } public static void main(String[] args) { Test t = new Test(); try{ t.fun(); }catch(SimpleException e){ e.printStackTrace(); } } }
运行就会得到下面的结果 printStackTrace是打印调用栈的方法,他有三个重载方法,默认的是将信息输出到System.err。这样我们就可以清晰的看到方法调用的过程,有点像操作系统中的中断,保护现场。
SimpleException: throwing from fun
at Test.fun(Test.java:4)
at Test.main(Test.java:9)
略微麻烦的语法
我们自己实现的异常有时候会用到继承这些特性,在异常继承的时候有一些限制。那就是子类不能抛出基类或所实现的接口中没有抛出的异常.比如有如下的接口:
public interface InterfaceA { public void f() throws IOException; }
我们的Test类实现这个接口,那么Test的f方法要么不抛出异常,要么只能抛出IOException,其实关于这里还有更琐碎的规矩,详细可以参考《Java Puzzlers》第37个谜题。所以这和传统的继承和实现接口正好相反,面向对象的继承是扩大化,而这正好是缩小了。