Java异常处理机制

          在C时代,程序处理错误的方式主要是通过判断函数返回值来处理的。采用这种方式要求每次调用函数都必须对返回值进行判断,并采取相应的处理措施,无疑给程序员带来了很大的工作量。所以,现实中程序员往往直接不对返回值采取错误检测。

          相对C而言,C++,Java都采用了所谓的异常处理机制来实现错误处理,当然C++为了向后兼容,代码中往往还会出现通过返回值进行错误处理的情况。

          异常处理机制,目的就是将错误产生的地方跟处理错误的地方分离开来,从而避免因为在有可能产生错误的代码中加入过多的错误检测,影响程序本身的执行逻辑。

一、Java异常机制基本语法

          异常机制可以分为三个部分:监控区域,抛出异常和捕获异常。        

package exceptions;

class ExceptionExample {

public static void throwException() throws Exception {

throw new Exception();

}

public static void main(String[] args) {

try {

throwException();

}

catch(Exception e) {

System.out.println(e);

}

}

}

/*Output:

java.lang.Exception

*/

在上面的代码中,抛出异常的部分是 throwException()方法。首先注意到我们通过

throw new Exception()抛出了一个Exception对象。另外,throwException()后面跟着的throws Exception是Java规定的异常说明。异常说明是方法说明的一部分,用来说明该方法可能产生的异常。其语法以throws关键字跟着可能产生的异常列表表示,如果有多个异常,则以逗号分隔。

           try关键字表示异常监控区域,一旦监控区域中的代码抛出异常,则代码执行序列将跳转到异常处理程序。异常处理程序是由多个catch子句表示的,catch括号内的参数表示要捕获的异常,后面跟着处理该异常的语句。

           Java是强制异常检查的,也就是说,throws异常说明列表中必须包含方法中有可能抛出的所有检查异常(检查异常在Java标准异常中定义),同时在catch子句中必须捕获这些异常类型,如果不符合,编译器将报错。

 

二、Java检查异常和不检查异常

Java中可抛出的异常必须是继承自Throwable类,在Java类库中有两个异常类型直接继承Throwable:

Error用来表示编译时的错误和系统错误,一般不会用到;Exception用来表示一个合理的程序应该对其进行捕获并处理的异常,所以我们通常用到的类库的异常或自定义的异常都是其子类。

           Java的Exception可以分为两类,检查异常和不检查异常。为了说明两者如何区分,我们首先必须介绍Exception的一个子类:RuntimeException。程序运行时Java虚拟机可能抛出的异常类型必须是RuntimeException或其子类。RuntimeException及其子类不用在方法的异常说明中声明,在catch子句中也不用捕获这种类型,所以称为不检查异常。

           相反,对于Exception类型或者Exception子类中不是继承自RuntimeException的类型,称为检查异常,因为它们必须在方法的异常说明中显示声明,并在catch子句中捕获。

 

三、自定义异常

           要创建自定义的异常,必须从已有的异常类继承,最好是选择意思接近的异常类继承。创建时,如果没有提供构造器,那么编译器将自动生成一个默认编译器,并且调用基类的构造器。          

class CustomizedException extends Exception {

private static final long serialVersionUID = 1L;

}

 

四、Throwable类

           因为所有的异常类都是继承Throwable的,所有有必要来了解一下Throwable提供什么信息。

1、Message细节信息

细节信息保存了关于异常类的说明,可以通过String getMessage()获得。另外还有一个方法

可以获得String getLocalizedMessage(),用户可以重写getLocalizedMessage从而生成自定义的消息格式,如果没有重写,则与getMessage输出相同。要设置Message必须通过构造函数。

2、StackTrace栈轨迹

            栈轨迹记录了程序从开始到异常抛出点所调用的方法。通过StackTraceElement[] getStackTrace()可以获得一个StackTraceElement的数组,每个StackTraceElement记录一个栈帧,0号元素表示栈顶,由栈的FILO规则可知0号元素表示最后调用的方法,而栈底则表示第一个调用的方法,一般就是main了。

            Throwable提供了一个打印栈轨迹的方法void printStackTrace(),输出到System.err,如要输出到其它地方,可调用printStackTrace(PrintStream s)或void printStackTrace(PrintWriter s)。

            重抛异常时,异常对象的所有信息保持不变,所以printStackTrace()西那是的将是原来异常抛出点的调用栈信息,而非重新抛出点的信息。要想更新这个信息,可以调用Throwable fillInStackTrace(),该函数将产生一个新的Throwable对象,它是通过把当前的调用栈信息填入原来那个异常对象而建立的。调用fillInStackTrace()的那一行成了异常的新发生地。不过fillInStackTrace()要生效必须保证StackTrace是可写的,这个可通过调用构造器Throwable(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace)实现。

3、cause因由

             当我们在捕获到一个异常后想抛出另一个异常,并且希望把原始的异常信息保存下来,就可以调用构造器把原始的异常对象保存到新的Throwable的cause中,如Throwable(String message, Throwable cause)。或者构造新的异常对象时没有初始化cause,则可以调用Throwable initCause(Throwable cause),这个方法最多只能调用一次,也就是说cause一旦初始化了就不能再设置。 可以把cause看成是链表元素中指向上一个元素的引用,所以最终将形成一条异常链。异常链的信息会在printStackTrace()中打印出来。

4、SuppressedException被屏蔽的异常

            保存被屏蔽的异常,可通过Throwable[] getSuppressed()获得。添加的话用addSuppressed(Throwable exception),这个函数一般是在try-with-resources语句中由自动调用的。

 

五、finally子句

            finally子句经常用来清理除内存外的其他资源,因为内存清理由垃圾回收自动释放。如下所示        

private static void customBufferStreamCopy(File source, File target) {

    InputStream fis = null;

    OutputStream fos = null;

    try {

        fis = new FileInputStream(source);

        fos = new FileOutputStream(target);

        byte[] buf = new byte[8192];

        int i;

        while ((i = fis.read(buf)) != -1) {

            fos.write(buf, 0, i);

        }

    }

    catch (Exception e) {

        e.printStackTrace();

    } finally {

        close(fis);

        close(fos);

    }

}

private static void close(Closeable closable) {

    if (closable != null) {

        try {

            closable.close();

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

}

 对于finally子句,有几点需要注意的:

1、无论try中的异常是否发生,finally子句都会在最后都会执行。

2、如果try子句中的语句正常执行到return语句,finally子句也会执行。

3、对于try子句如果有finally子句,则可以不用catch子句。

 

六、try-with-resources语句

            从 Java 7 build 105 版本开始,Java 7 的编译器和运行环境支持新的 try-with-resources 语句,称为 ARM 块(Automatic Resource Management) ,自动资源管理。使用如下:

private static void customBufferStreamCopy(File source, File target) {

    try (InputStream fis = new FileInputStream(source);

        OutputStream fos = new FileOutputStream(target)){

        byte[] buf = new byte[8192];

        int i;

        while ((i = fis.read(buf)) != -1) {

            fos.write(buf, 0, i);

        }

    }

    catch (Exception e) {

        e.printStackTrace();

    }

}

            在使用try-with-resources语句的时候,异常可能发生在try语句中,也可能发生在释放资源时。如果资源初始化时或try语句中出现异常,而释放资源的操作正常执行,try语句中的异常会被抛出;如果try语句和释放资源都出现了异常,那么最终抛出的异常是try语句中出现的异常,在释放资源时出现的异常会作为被抑制的异常添加进去,即通过Throwable.addSuppressed方法来实现。

              能够被try语句所管理的资源需要满足一个条件,那就是其Java类要实现java.lang.AutoCloseable接口,否则会出现编译错误。当需要释放资源的时候,该接口的close方法会被自动调用。Java类库中已有不少接口或类继承或实现了这个接口,使得它们可以用在try语句中。在这些已有的常见接口或类中,最常用的就是与I/O操作和数据库相关的接口。与I/O相关的java.io.Closeable继承了AutoCloseable,而与数据库相关的java.sql.Connection、java.sql.ResultSet和java.sql.Statement也继承了该接口。如果希望自己开发的类也能利用try语句的自动化资源管理,只需要实现AutoCloseable接口即可。代码清单1-19给出了一个自定义资源的使用示例,在close方法中可以添加所需要的资源释放逻辑。

 

七、异常匹配

           抛出异常时,catch子句会按照代码的先后顺序找出最先遇到的匹配的处理程序执行。需要注意的是超找的时候并不要求抛出的异常同处理程序所声明的异常完全匹配,派生类的异常对象可以匹配其基类的处理程序,所以安排catch子句顺序时应考虑先派生类后基类。

 

八、异常说明的继承。

          前面说过异常说明是方法声明的一部分,那么如果派生类想覆盖基类的某个包含异常说明的方法,需要如何处理呢?这个问题要分为两种情况,一种是对于构造函数,另外一种是对于成员方法。

          对于覆盖成员方法的情况,为了满足多态的情况,我们必须保证处理基类方法产生异常的程序能处理派生类方法所能产生的异常(因为我们可以把派生类向上转型为基类),这就要求派生类方法的异常说明中的异常必须包含于基类方法的异常说中。

          对于构造函数,我们调用的时候对象还没有生成,所以不能将其赋予一个基类引用,不存在多态的情况,所以派生类构造函数的异常说明中可以包含基类构造函数的异常说明中没有的异常类型。另一方面,由于派生类的构造函数会调用基类的构造函数,会产生基类构造函数能产生的异常,所以派生类构造函数的异常说明应包含基类构造函数的异常说明中的异常类型。这跟成员方面的情况刚好相反。

 

posted @ 2012-11-23 00:06  chanon  阅读(403)  评论(0编辑  收藏  举报