java深入浅出解析异常机制

 

版权声明:本文为博主原创文章,转载请注明原地址,谢谢 https://blog.csdn.net/QuinnNorris/article/details/57428399

java中的异常处理的目的在于通过使用少量的代码,使得程序有着强大的鲁棒性,并且这种异常处理机制会让你变得非常自信:你的应用中没有你没处理过的错误。处理异常的相关手法看起来是这么的难懂,但是如果掌握的话,会让你的项目收益明显,效果也会是立竿见影。 
如果我们不使用异常处理,那么我们在程序中必须检查特定的错误,并且在程序的很多地方去处理他,这样会带来低效率和高耦合。这是我们不希望看到的。有了异常处理,我们可以不必在方法调用处进行检查,因为异常机制将保证能够捕获这个错误。并且,只需在一个地方处理错误。

(一)解决异常情形的基本思路

1.普通问题和异常情形

异常情形是指阻止当前方法或作用域继续执行的问题。我们首先需要区分普通问题和异常情形,普通问题是在编程的过程中,我们可以通过已知的信息解决,并继续执行的问题。异常情形是指在当前的情况下,我们不能继续下去了,在当前的环境下我们不能解决这个问题,我们只能将这个问题抛出当前的环境,到一个大的环境中去企图解决这个问题,这就是抛出异常时发生的事情。

2.抛出异常之后

我们在区分了普通问题和异常之后说到,如果我们没有能力处理的问题就需要抛出异常。在抛出异常的时候,会有几件事情发生:

  1. java用new在堆上来创建一个异常对象。
  2. 当前执行的程序不能被执行下去,程序被终止之后,从当前环境弹出一个对异常对象的引用。
  3. 异常处理机制接管程序,试图找到一个恰当的地方来执行异常处理。

异常处理使得我们可以将每件事情都看作一个事物来处理,而异常可以看作这些事务的底线。我们还可以将异常看作是一种内建的恢复系统,因为我们的程序中可以拥有各种不同的恢复点。如果程序的某部分事物都失败了,我们至少可以返回一个稳定的安全处。

3.两种异常处理的基本模型

在所有现存的语言中,对于处理异常有两种基本模型。 
java支持的模型是终止模型,(c++,c,python,c#也是如此)。在这种模型中,假设错误非常关键,以至于程序无法返回到异常发生的地方继续执行程序。一旦异常被抛出,说明程序已经无法挽回,不可能回到原处继续进行了。 
另外一种称为恢复模型,这种模型认为异常处理程序的工作是修正错误,然后重新尝试调用出问题的地方,并认为能够二次成功。

虽然恢复模型开始显得很吸引人,但不是很实用。其中的主要原因可能是它所导致的耦合,因为恢复性的处理程序需要了解异常抛出的地点,这一点是致命的。我们怎么才能告知程序我的代码在哪里出错呢?这势必要包含了非通用性的高耦合代码,这增加了编写和维护的难度。

(二)捕获异常

就像我们上面说的,在抛出异常的时候我们总是用new在堆上创建异常对象,这也伴随着储存空间的分配和构造器的调用。所有的标准异常类都有两个构造器:一个是默认的构造器,另一个是接受字符串作为参数。

1.将异常对象看作“返回”

关键词throw会产生很多有趣的结果。在使用new创建了异常对象之后,此对象的引用将会传递给throw。尽管返回异常对象其类型通常与方法设计的返回类型不同,但从效果上看,我们可以假装认为从这个方法或代码块“返回”了一个异常对象给throw。

另外,我们可以抛出任意类型的Throwable对象,他是异常类的根类(祖先类)。错误信息可以保存在异常对象内部或者用异常类的名称来表示。

2.try块

如果代码中抛出异常,那么我们的程序将会终止,如果不希望程序就此结束,我们可以通过try-catch块来操作。在try块中的内容如果抛出了异常,我们只会结束try块中运行的内容,而不会结束整个程序。

3.catch块

catch块就是我们刚才一直提到的异常处理程序。catch块在try块之后,我相信try-catch大家都知道,这里也就不把这种基本的东西介绍个没完了。需要注意的是,在try块内部,可能有几个方法都会抛出同一个异常,我们只需写一个这种异常的catch块就可以捕获所以,无需重复书写。而且,catch块要按照从细小到广泛的顺序来写,如果我们把Exception放在第一个,那么剩下的具体的异常catch块将捕获不到异常,因为Exception可以处理所有的。

关于catch块的另外两个小知识点: 
1.可以用|来合并那些虽然异常不同,但是操作相同的catch块:

catch(FileNotFoundException | IOException e)
{
    //如果这两个异常的操作是一样的,我们可以把他们的操作写在一起,从jdk7开始
}

2.如上捕获多个异常的时候,异常变量隐含为final变量。不能为上面的代码的e赋不同的值。

4.创建一个自己定义的异常类

我们先创建一个自己定义的异常类,我们可以看到,异常类有这样的两种构造器方法,一种是默认的无参数构造方法,另外一种是传递一个String类型的出错原因的构造器。(异常一共有4种构造器方法,这里只说了两种)我们什么都不用做,因为我们现在只是简单的看一下,具体的内容我们以后再来添加。

package ExceptionEx;

/**
 * 
 * @author QuinnNorris
 * 
 *         自定义异常类
 */
public class MyException extends Exception {

    public MyException() {

    }

    public MyException(String msg) {
        super(msg);
    }

}

之后,我们要创建一个测试类,在这个测试类中我们会让一个方法手动的抛出一个异常。这种方法只是为了演示,在实际的情况下,异常对象地抛出都是因为我们的编译问题由编译器为我们抛出的。在我们用方法抛出一个异常后,我们会用try-catch块来自己接住这个异常。

package ExceptionEx;

/**
 * 
 * @author QuinnNorris
 * 
 *         测试类,用方法抛出异常
 */
public class TestExcep {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        try {
            throwMyExce();
        } catch (MyException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    public static void throwMyExce() throws MyException {
        throw new MyException();
    }

}

输出的结果是我们出错的栈轨迹,之所以会输出这些内容,是我们在catch块中调用printStackTrace方法的结果。

输出结果: 
ExceptionEx.MyException 
at ExceptionEx.TestExcep.throwMyExce(TestExcep.java:20) 
at ExceptionEx.TestExcep.main(TestExcep.java:12)

(三)异常类继承关系树

1.异常类关系图

java异常类关系树 
看一张图,一看就懂。

2.Exception和Error类

在java中,所有的异常都有一个共同的祖先 Throwable类。而Throwable类有两个子类,一个是Error类,一个是Exception,这两个哥俩分别管两种不同类型的错误。

Error类主要负责“错误”。 
这个错误和异常有很大的区别。你可以看见我们上面一直使用的名词是异常而不是错误。因为在java中,能通过代码处理的我们叫做异常,而我们不能处理的才叫做错误。错误指的是那些例如:JVM运行错误,栈空间用尽,类定义错误等等非常严重的问题。这种问题我们是无法去处理的,也就是说,程序一旦出现Error我们和编译器都是没办法解决的。java只能尽全力去试图让代码远离error,但是如果发生,我们都无能为力了。

Exception类主要负责“异常”。 
这个类中的问题我们是可以去通过代码处理的。所以有的时候我们说异常处理的这种机制,往往会想到Exception。这是没问题的,而且其实Throwable类的方法和Exception是相同的,连构造器都是那么类似,Exception主要负责处理类似:空指针、除数为零、数组越界这类问题。

2.运行时异常、非运行时异常

在Exception中,我们通常又将异常分为两类,RunTimeException和其他的异常。其他的异常也有很多种,但是我们为什么把运行时异常(RunTimeException)单独提出来作为一类呢?因为这里有本质性的区别。

RunTimeException表示那些逻辑性错误,可以避免。 
运行时异常都包含那些呢?我们看两个例子就懂了:NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)。在我们编写代码的时候,我们从来不会希望编写出有空指针或者下标越界的代码,因为这是错误的,在java中,这些逻辑上的,因为我们大意而造成的错误都叫做运行时错误。运行时错误是不会在程序中用try-catch块来声明的,因为如果出现这种错误我们会修改自己的代码纠正错误,而不是用try-catch块来捕获。

注:如果你非要用try-catch块来包含自己的代码免受RunTimeException的困扰,从理论上编译器是不会报错的。但是这样只会让你的代码变得冗长,而且你会因此而不知道你的逻辑到底有没有问题。这是一种非常不好的行为。

非运行时异常表示有可能发生的异常,我们需要声明。 
有的时候我们会遇到这种情况:在使用文件操作的时候,突然有一句语法正确的话被编译器报错,当鼠标移动到上面的时候发现,哦,原来是让我throw或者try-catch。这种就是非运行时异常。为了防止代码在运行时出现问题,java强制规定:非运行时异常必须被处理。这种强制规定的原因前面已经讲过了,总之,当要使用文件或者SQL语句的时候,出现例如文件找不到,数据库失败这样的问题是非常有可能的,我们必须抛出或处理这种可能会出现的异常。

一句话来总结:运行异常是程序逻辑错误,无法预料,改正就可以,无需抛出或处理。非运行时异常是显然可能存在的错误,我们被强制必须抛出或处理来保证程序的安全性。

3.已检查的异常、未检查的异常

通常,Java的异常(Throwable)又可以被分为已检查异常(checked exceptions)和未检查异常(unchecked exceptions)这两种。其实这两种和我们上面的差不多,我们在这里给出一下概念:

已检查异常: 
包括非运行时异常。在程序中是可以被检查的,需要我们处理和预防的。

未检查的异常: 
包括RuntimeException和Error。我们在程序和逻辑上尽量避免出现这种异常,如果出现这种异常我们是未知的。

(四)Execption类

上面我们分析了Throwable类的两个子类,既然我们不去管Error,我们来看一下Exception类的结构。

1.getMessage、getLocalizedMessage

我们刚才说过,Exception类有这样两种构造器(异常类一共四种构造器),有参数的构造器有一个String的参数,是用来存储错误信息的,那么既然能够存储信息,也肯定有读取信息的方法。

public String getMessage()
//返回此 throwable 的详细消息字符串。 

public String getLocalizedMessage()
//创建此 throwable 的本地化描述。子类可以重写此方法,以便生成特定于语言环境的消息。
//对于不重写此方法的子类,默认实现返回与 getMessage() 相同的结果。 

我们通过这两种方法来获得存储着信息的字符串。

2.printStackTrace

printStackTrace这个方法有三种重载的形态,总的来说,这个方法的作用是输出错误信息。

public void printStackTrace()
//将此 throwable 及其追踪输出至标准错误流。
//此方法将此 Throwable 对象的堆栈跟踪输出至错误输出流,作为字段 System.err 的值。
//输出的第一行包含此对象的 toString() 方法的结果。
//剩余行表示以前由方法 fillInStackTrace() 记录的数据。

public void printStackTrace(PrintStream s)
//将此 throwable 及其追踪输出到指定的输出流。 

public void printStackTrace(PrintWriter s)
//将此 throwable 及其追踪输出到指定的 PrintWriter。 

我们给出一个API中的例子:

 class MyClass {
     public static void main(String[] args) {
         crunch(null);
     }
     static void crunch(int[] a) {
         mash(a);
     }
     static void mash(int[] b) {
         System.out.println(b[0]);
     }
 }

输出b[0]的时候,这个数组是不存在的,肯定是一个空指针的错误。通过上面的这个这个例子会产生以下的错误。

 java.lang.NullPointerException
         at MyClass.mash(MyClass.java:9)
         at MyClass.crunch(MyClass.java:6)
         at MyClass.main(MyClass.java:3)

3.fillInStackTrace

在上一个printStackTrace显示栈轨迹的方法中有说过,栈轨迹是存储在这个方法中的。

public Throwable fillInStackTrace()
//在异常堆栈跟踪中填充。此方法在 Throwable 对象信息中记录有关当前线程堆栈帧的当前状态。 
  • 1
  • 2

4.getStackTrace、setStackTrace

我们还有以数组的形式获得和修改栈轨迹的方法,但是在一般情况下,我们几乎不会用到,这里就展示一下。

public StackTraceElement[] getStackTrace()
//提供编程访问由 printStackTrace() 输出的堆栈跟踪信息。
//返回堆栈跟踪元素的数组,每个元素表示一个堆栈帧。
//数组的第零个元素(假定数据的长度为非零)表示堆栈顶部,它是序列中最后的方法调用。

public void setStackTrace(StackTraceElement[] stackTrace)
//设置将由 getStackTrace() 返回,并由 printStackTrace() 和相关方法输出的堆栈跟踪元素。 
//此方法设计用于 RPC 框架和其他高级系统,允许客户端重写默认堆栈跟踪,

(五)如何捕获全部异常?

我们刚才说了如何去捕获异常,也分析了Exception类中的方法,那么我们现在要提出一个有实际性意义的问题:如何去捕获全部的异常?你可能会不理解这句话的意思,但实际上,我们在日常编写代码运行的时候有可能因为几次方法的调用、不同异常的抛出导致最开始抛出的异常丢失。如果我们要将异常用日志的形式记录下来,这种异常情况的丢失是我们不能容忍的。

1.栈轨迹

栈轨迹是一种概念。我们的异常存放于一个异常堆栈中,这个栈有栈顶,和一帧帧的位置来保存异常被抛出的路径。

 java.lang.NullPointerException
         at MyClass.mash(MyClass.java:9)
         at MyClass.crunch(MyClass.java:6)
         at MyClass.main(MyClass.java:3)

这是printStackTrace方法打印出来的栈轨迹,这个方法返回一个由栈轨迹中的元素所构成的数组。每个元素表示栈中的一个帧。数组中的最后一个元素也是栈底也是调用序列中离抛出异常最远的第一个方法调用。

2.重抛异常

在很多个方法递归调用的情况下,有的时候我们用try-catch块接住了异常,但是我们并不想在这个方法中处理,而是想把它继续向上抛出,到上一个块中处理。那么这个时候我们就需要重新抛出异常

重抛异常会把异常抛给上一级环境中的异常处理程序,同一个try块的后续catch子句将会被忽略。此外,异常对象的所有信息都得以保持。所以高一级的环境中捕获此异常的处理程序可以从这个异常对象中得到所有信息。

需要注意的是,如果我们多次重抛异常,在这之后我们再调用printStackTrace()方法,现实的栈轨迹是全部的轨迹,而不是最后一次抛出时开始的轨迹,这是我们希望的情况。同时,还有另外一件事,fillInStackTrace()方法会返回一个异常对象,会刷新当前的栈轨迹,也就是说我们调用fillInStackTrace的话,那一行就变成了异常的新发生地了。那么这两件事情结合在一起,我们得出了一个结论:多次重抛同一个异常,不会调用fillInStackTrace方法刷新栈轨迹。但是还有另外的情况,就是我们在捕获异常之后抛出了另一个异常。因为这样做会替换那个异常对象的堆栈,就相当于使用了fillInStackTrace()方法,重抛了不同的异常,会导致有关原来的异常发生点的信息会丢失,剩下的是新的异常的抛出点的信息

package Test;  

/**
 * 
 * @author QuinnNorris
 * 
 * 重抛异常测试类
 */
public class Test{  
    public Test() {  
    }  

    void testEx() throws Exception {  
        try {  
            testEx1();  
        } catch (Exception e) {  
            throw e;  
        }
    }  

    void testEx1() throws Exception {   
        try {  
           testEx2();  
        } catch (Exception e) {  
            //throw e;
            //throw new Exception();
        }
    }  

    void testEx2() throws Exception {  
        try {  
            int b = 1;  
            int c;  
            for (int i = 2;; i--) {  
                c = b / i;  
            }  
        } catch (Exception e) {  
            throw e;  
        } 
    }  

    public static void main(String[] args) throws Exception {  
        Test testException1 = new Test();   
            testException1.testEx();  
    }  
}  

这个代码模拟了我们平时工作的方法多层调用的情况。

throw e;
  •  

我们先把这个代码中注释掉的第一行去掉注释。

输出结果: 
Exception in thread “main” java.lang.ArithmeticException: / by zero 
at Test.Test.testEx2(Test.java:35) 
at Test.Test.testEx1(Test.java:23) 
at Test.Test.testEx(Test.java:15) 
at Test.Test.main(Test.java:44)

这个时候,它的栈轨迹是这样的,这个是完全的从除零错误开始的站轨迹。

throw new Exception();
  • 1

这个时候我们再一次注释掉第一行,我们把第二行的注释去掉。

输出结果:Exception in thread “main” java.lang.Exception 
at Test.Test.testEx1(Test.java:26) 
at Test.Test.testEx(Test.java:15) 
at Test.Test.main(Test.java:44)

完全变成了另外一个异常。这说明了这种重抛异常的时候可能发生的问题,可能上面的例子确实很简单,但是实际中,当我们许多个方法一调用可能就会被这种情况搞糊涂了。

3.异常链

常常会想要在捕获一个异常后跑出另一个异常,并且希望能将原来的异常信息保存下来,这种我们称之为异常链。在Throwable的子类构造器中都有一个可以接受一个叫做cause的Throwable对象作为参数,表述原始的那个异常。但是这个构造器只有几个异常类(Throwable,Error,Exception,RuntimeException)拥有,更普遍的写法是调用initCause方法。

public Throwable(Throwable cause)
//构造一个带指定 cause 和 (cause==null ? null :cause.toString())的详细消息的新 throwable。
//此构造方法对于那些与其他 throwable(例如,PrivilegedActionException)的包装器相同的 throwable 来说是有用的。 
//调用 fillInStackTrace() 方法来初始化新创建的 throwable 中的堆栈跟踪数据。 


public Throwable(String message,Throwable cause)
//构造一个带指定详细消息和 cause 的新 throwable。
//注意,与 cause 相关的详细消息不是 自动合并到这个 throwable 的详细消息中的。 
//调用 fillInStackTrace() 方法来初始化新创建的 throwable 中的堆栈跟踪数据。 

public Throwable initCause(Throwable cause)
//将此 throwable 的 cause 初始化为指定值。(该 Cause 是导致抛出此 throwable 的throwable。) 
//此方法至多可以调用一次。此方法通常从构造方法中调用,或者在创建 throwable 后立即调用。
//如果此 throwable 通过 Throwable(Throwable) 或 Throwable(String,Throwable) 创建,此方法不能调用。 

我们需要注意的是最后一行写出的,initCause和构造器方法不兼容的情况,其他的没问题。 
下面我们要举出一个使用异常链的具体例子,这个例子是来自thinking in java的,可能比较长,但他确实比其他书中的例子更加直观和有实际意义。

package ExceptionEx;

/**
 * 
 * @author QuinnNorris
 * 
 *         继承了Exception的自定义异常类
 */
class DynamicFieldException extends Exception {
}

/**
 * 
 * @author QuinnNorris
 * 
 *         包含一个对象数组以及一些操作的主类
 */
public class DynamicFields {

    private Object[][] fields;

    /**
     * 用传入的大小参数设定初始化数组的行数
     * 
     * @param initialSize
     */
    public DynamicFields(int initialSize) {
        fields = new Object[initialSize][2];
        for (int i = 0; i < initialSize; i++)
            fields[i] = new Object[] { null, null };
    }

    /**
     * 重写toString,以为了能够输出数组
     */
    public String toString() {
        StringBuilder result = new StringBuilder();
        for (Object[] field : fields) {
            result.append(field[0]);
            result.append(": ");
            result.append(field[1]);
            result.append("\n");
        }
        return result.toString();
    }

    /**
     * 查询是否有是id的field[n][0]
     * 
     * @param id
     * @return
     */
    private int hasField(String id) {
        for (int i = 0; i < fields.length; i++)
            if (id.equals(fields[i][0]))
                return i;
        return -1;
    }

    /**
     * 查询id的索引,如果有就返回索引,如果没有就抛出异常
     * 
     * @param id
     * @return
     * @throws NoSuchFieldException
     */
    private int getFieldNumber(String id) throws NoSuchFieldException {
        int idIndex = hasField(id);
        if (idIndex == -1)
            throw new NoSuchFieldException();
        return idIndex;
    }

    /**
     * 查看全部的fields,将一个新的id添加进去。如果没有空位置就将数组添加一行返回索引。
     * 
     * @param id
     * @return
     */
    private int makeField(String id) {
        for (int i = 0; i < fields.length; i++)
            if (fields[i][0] == null) {
                fields[i][0] = id;
                return i;
            }
        Object[][] tmp = new Object[fields.length + 1][2];
        for (int i = 0; i < fields.length; i++)
            tmp[i] = fields[i];
        for (int i = fields.length; i < tmp.length; i++)
            tmp[i] = new Object[] { null, null };
        fields = tmp;
        return makeField(id);
    }

    /**
     * 获取id的那一行储存的值
     * 
     * @param id
     * @return
     * @throws NoSuchFieldException
     */
    public Object getField(String id) throws NoSuchFieldException {
        return fields[getFieldNumber(id)][1];
    }

    /**
     * 从id中取出值
     * 
     * @param id
     * @param value
     * @return
     * @throws DynamicFieldException
     */
    public Object setField(String id, Object value)
            throws DynamicFieldException {
        if (value == null) {
            DynamicFieldException dfe = new DynamicFieldException();
            dfe.initCause(new NullPointerException());
            throw dfe;
        }
        int fieldNumber = hasField(id);
        if (fieldNumber == -1)
            fieldNumber = makeField(id);
        Object result = null;
        try {
            result = getField(id);
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        }
        fields[fieldNumber][1] = value;
        return result;
    }

    /**
     * @param args
     */
    public static void main(String[] args) {
        DynamicFields df = new DynamicFields(3);
        System.out.println(df);

        try {
            df.setField("d", "A value for d");
            df.setField("number", 47);
            df.setField("number2", 48);
            System.out.println(df);
            df.setField("d", "A new value for d");
            df.setField("number3", 11);
            System.out.println("df: " + df);
            Object field = df.setField("d", null);
        } catch (DynamicFieldException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }

}

每个对象都含有一个数组,元素是”成对的对象“。第一个对象表示字段标识符,第二个标识字段值,值的类型可以是除基本类型外的任意类型。当调用setField方法的时候,她将试图通过标识修改已有字段值,否则就建一个新的字段,并把值放入。如果空间不够了,将建立一个更长的数组,并把原来的数组的元素复制进去。如果你试图为字段设置一个空值,将抛出一个DynamicFieldException异常,它是通过使用initCause方法把NullPointerException对象插入而建立的。

在这里我们还有另外一个很重要的技巧:

public Object setField(String id, Object value)
            throws DynamicFieldException {
        if (value == null) {
            DynamicFieldException dfe = new DynamicFieldException();
            dfe.initCause(new NullPointerException());
            throw dfe;
        }
        int fieldNumber = hasField(id);
        if (fieldNumber == -1)
            fieldNumber = makeField(id);
        Object result = null;
        try {
            result = getField(id);
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        }

或许你会为上面这段代码感到疑惑,为什么我们不直接在方法名后面声明NoSuchFieldException异常,而是捕获这个异常之后,将他变成RuntimeException。原因是这样的:如果在一个方法中发生了一个已检查异常,而不允许抛出它,那么我们可以捕获这个已检查异常,并将它包装成一个运行时异常。这个方法在这里看来有些多余,但是这种将已检查异常包装在未检查异常里的手法非常实用,在后面我们会介绍到,如果我们继承的方法没有声明抛出异常,我们是不能抛出异常的,到那个时候,我们会需要到这种手法。

(六)使用finally进行清理

1.finally作用

对于一些代码,可能会希望无论try块中的异常是否抛出,它们都能得到执行。为了达到这个效果,可以在异常处理程序后面加上finally子句。对于没有垃圾回收和析构函数自动调用机制的语言来说,finally非常重要。它能使程序员保证:无论try块中发生什么,内存总能得到释放。

但是在java中有垃圾回收机制,所以内存释放不再是问题。所以,更多的时候,当要把除了内存以外的资源恢复到它们的初始状态时,就要用到finally子句。这种资源包括:已经打开的文件或网络资源,在频幕上画的图形等等。稍微有一些项目经验的朋友就知道finally到底多么好用。

2.带资源的try块

在很多的情况下,我们用finally只是为了简单的将资源关闭。再注意到这种情况后,java se7为这种情况提供了一个很有用的快捷方式。可以为我们快速简洁的关闭用.close方法关闭的资源。

try(Resource res = ...)
{
    //do some work
}

在这种写法之下,try快退出时,会自动调用res.close()方法。但是这种写法仅仅在是“close”方法的时候可用,比如多线程中的ReentrantLock的关闭方式不是调用close方法,那这就不适用。

3.finally的异常丢失问题

遗憾的是,java中的异常实现有一些瑕疵。异常作为程序出错的标志,绝对不应该被忽略,如果被忽略会给调试者带来很大的麻烦。但是,请考虑这种情况:在try中调用了方法,这个方法抛出一个一场,但是在finally中又调用了其他的一个方法,这个新方法也抛出一个异常。理论上,当代码遇到异常的时候,会直接停止现在的工作,抛出异常,但是finally这种特殊的机制,导致又抛出了一个异常,而且这种抛出直接导致前面的异常被覆盖了。

甚至还有更令人绝望的问题,比如,你可以试着敲一下下面这段代码。

package ExceptionEx;

public class FinallyEx {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        try {
            throw new RuntimeException();
        } finally{
            return;
        }

    }

}

你会发现,这个程序不会产生任何输出。 
这是一种相当严重的缺陷,因为异常可能会以一种比前面的例子更加微妙和难以察觉的方式完全丢失。在平时的项目中,我们要做的就是尽量少的把逻辑性代码放入finally中,finally最主要的作用还应该是关闭资源。

4.finally块中的代码一定会执行吗?

所有的书上都在说,finally块一定会被执行,但是如果你去面试,面试官很有可能会问你:“finally块一定会被执行么?”这个时候,你就蒙了。事实上finally块在一种情况下不会被执行:JVM被退出。虚拟机都被推出了,什么都不会执行了。

package ExceptionEx;

public class FinallyEx {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        try {
            System.exit(0); 

        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }finally{
            //do some thing;
            System.out.println("111");
        }

    }

}

如果调用System.exit(0)那么你会发现不会再打印出111这个字符串了。还有请不要在抛出异常后调用这个退出jvm的方法,编译器会报一个:Unreachable code(代码不会被执行)的错误。

(七)异常抛出的限制

如果在子类中覆盖了超类的一个方法,子类方法中声明的已检查异常不能比超类方法中声明的异常更通用。特别需要说明的是,如果超类方法没有抛出任何已检查异常,子类也不能抛出任何已检查异常。

1.如何处理这种限制

这个时候我们或许会要用到上文中提到过的包装方法。将已检查异常当作未检查异常偷梁换柱抛出去。所以有的时候我们说:不要去抛出任何未检查异常,这么绝对的说也是不好的。

(八)异常机制使用技巧

目前还是存在着大量的对于如何恰当的使用异常机制的争论。有很多人认为异常还不够,但是也有很多人认为这种繁琐的机制应该直接被去掉。下面有一些关于异常机制使用的技巧:

1.异常处理不能代替简单测试

我们有很多的情况下需要判断一个值是不是为空,根据它的情况来进行下一步的操作。在这种时候,我们还是用if-else的判断更佳,如果这种简单测试都用try-catch来捕获,那么这代码也确实过于冗杂。

2.不要过分的细化也不要只捕获Throwable

很多程序员都习惯将每条语句都放在一个try块中,这样的结果只会导致代码量的急剧膨胀。不要过分的细化每个异常,但是我们也不要只捕获Throwable,只抛出RuntimeException异常。这样也会让你的代码更加的难以理解。

3.不要压制异常

在java中既然有try-catch,我们常常会直接在方法内部关闭掉这个异常,直接把异常压制在非常低的等级中。这不是非常好的做法。尽管有的异常我们不需要处理,但是还有一些异常,抛出来的话我们才能知道到底是哪里出了问题。

4.不要害怕抛出异常,“苛刻”的检查比“放任”好

抛出异常不代表程序出了bug,抛出很多异常不代表我们代码问题很多,如果放任异常往往导致要花费更多的时间去慢慢的找问题出现的地点。报错或抛出异常总是比结果错误好。不仅如此,自己抛出一个EmptyStackException总是比java自己抛出一个NullPointerException好。

猜你喜欢

转载自blog.csdn.net/xaccpJ2EE/article/details/83185763