程序的异常处理在开发中很是常见,只有正确处理好意外情况,才能保证程序的可靠性。本文通过 Java 异常处理机制,详细的分析与解释 Java 中的 Exception 与 Error 之间的区别以及需要注意的点,以便更好的实现程序的可维护性。

如何区分 Exception 和 Error

在 Java 的异常处理机制中,有许多捕获由于产生的不同异常而制定不同的异常类,各异常类之间的关系如下图所示:

image.png

从上图可以看出,ExceptionError都继承了Throwable类。一般我们在编写代码的时候,假如语法使用错误,编辑器通常会给与提醒,我们是很容易发现并改正的。但在编译期间发生的错误是不能全部找到的,也就是说,某一些错误只有在程序运行期间才能显现出来,这类错误就属于Throwable,它可以被抛出或者被捕获,同时也是异常处理机制的基本组成类型。

对于异常的抛出,首先会在堆上创建异常对象,然后当前执行的路径会被终止,并且从当前环境中弹出对异常对象的引用,最后的程序由异常处理机制进行接管,从而将程序从错误状态中恢复。

下面对ExceptionError进行展开说明:

Error是指在正常情况下,不大可能出现的情况,它属于程序本身无法处理的错误,通常与 JVM、环境资源有关。因为假如发生了此类情况,它将会导致程序处于非正常的、不可恢复的状态,一般会终止线程。这些错误是不能事先知道的,或者说是不可查的,并且处于应用程序的控制和处理能力之外。常见的比如 OutOfMemoryError 类。

Exception属于程序本身可以进行处理的错误,可以在程序运行时将故障进行抛出。从图中可以看到,它含有:

  • 运行时异常:例如常见的 NullPointerExceptionIndexOutOfBoundsExceptionClassCastException 等;
  • 其它异常(也称非运行时异常):例如常见的 IOExceptionSQLException 等。

将以上的异常进行分类的话,通常分为:

  • 非检查异常(unchecked exception):包括 RuntimeException 及其子类Error,此类异常在编译时不要求必须进行异常捕获或抛出,即假如程序中可能出现此类异常,即使没有进行捕获或者抛出,也会编译通过。

  • 检查异常(checked exception):此类异常必须进行处理,应进行捕获(try-catch)或者抛出(throws),只有这样编译器才会通过。

如何处理异常

一般情况下可以使用try-catch语句进行异常的处理,如下所示:

1
2
3
4
5
6
7
8
9
try {
    // 可能发生异常的语句
} catch(ExceptionType1 ExceptionName1) {
    // 处理 ExceptionType1 类型的异常
} catch(ExceptionType2 ExceptionName2) {
    // 处理 ExceptionType2 类型的异常
} catch(ExceptionType3 ExceptionName3) {
    // 处理 ExceptionType3 类型的异常
}

try中发生异常的时候,异常处理机制会负责寻找与当前异常相匹配的异常类型,然后进入对应的catch中执行相应的异常处理语句,一旦异常得到处理(catch语句执行结束),则异常处理程序就结束了。也就是说,如果没有发生异常,则catch语句块不会执行,如果发生了异常,则按照catch语句块的先后顺序进行匹配,只要匹配成功,就执行catch中的处理逻辑,那么后面的catch就不会执行了。

其中catch子句必须与try子句同时使用,假如引入了finally子句,则会存在以下三种格式:

当然,try语句块只能有一个,catch语句块可以有多个,finally语句块最多只能有一个,也可以没有。

1
2
3
4
5
try {

} catch {

}
1
2
3
4
5
6
7
try {

} catch {

} finally {

}
1
2
3
4
5
try {

} finally {

}

但需要注意一点的是:finally子句总是会执行的,但前提条件是try子句执行。如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class ExceptionAndErrorTests {
    public static void main(String[] args) {
        System.out.println("return value of test(): " + test());
    }

    public static int test() {
        int i = 1;
        System.out.println("the previous statement of try block");
        i = i / 0;

        try {
            System.out.println("try block");
            return i;
        } finally {
            System.out.println("finally block");
        }
    }
}

输出结果如下:

1
2
3
4
the previous statement of try block
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at _01_BasicClass.ExceptionAndErrorTests.test(ExceptionAndErrorTests.java:11)
	at _01_BasicClass.ExceptionAndErrorTests.main(ExceptionAndErrorTests.java:5)

由于没有执行try语句,所以finally子句也没有执行。

为了弄清楚这些语句与return语句(控制转移语句)之间的关系,下面给出一些例子以进行区分,相关引用已在参考中给出链接:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class ExceptionAndErrorTests {

    public static void main(String[] args) {
        try {
            System.out.println("try block");
            return ;
        } finally {
            System.out.println("finally block");
        }
    }
}

输出结果如下:

1
2
try block
finally block

从执行结果可看出,假如try中没有发生异常的语句,则finally语句会在try中的return语句执行之前执行。再看下面代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class ExceptionAndErrorTests {
    public static void main(String[] args) {
        System.out.println("return value of test() : " + test());
    }

    public static int test() {
        int i = 1;

        try {
            System.out.println("try block");
            i = 1 / 0;
            return 1;
        } catch (Exception e) {
            System.out.println("exception block");
            return 2;
        } finally {
            System.out.println("finally block");
        }
    }
}

输出结果如下:

1
2
3
4
try block
exception block
finally block
return value of test() : 2

从结果可以看出,在进入try块后,首先打印了try block,由于发生了异常,会被catch捕获,继而输出exception block,最后输出finally block语句。

值得注意的是,test()方法最终返回的是2,这就说明finally语句是在catch中的return执行之前执行的。同时,当i = 1 / 0;语句发生异常后,紧跟着后面的return 1;语句也就不会执行了。此时,如果将return 1;return 2;任意一个注释掉的话,则会提示 缺少返回语句。再看如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class ExceptionAndErrorTests {
    public static void main(String[] args) {
        int i = 1;
        try {
            System.out.println("try block1");
            i = 1 / 0;
            System.out.println("try block2");
        } finally {
            return;
        }
    }
}

输出结果如下:

1
try block1

从结果可以看出,在finally块中使用了return;语句的前提下,即使try捕获到了异常,也会执行finlly块。

综上所述,finally块是在try或者catch中的return语句之前执行的,类似于return的控制转移语句还有breakcontinuethrow。再看最后一段代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class ExceptionAndErrorTests {

    public static void main(String[] args) {
        System.out.println(test());
    }

    public static String test() {
        try {
            System.out.println("try block");
            return test1();
        } finally {
            System.out.println("finally block");
        }
    }

    public static String test1() {
        System.out.println("return statement");
        return "after return";
    }
}

输出结果如下:

1
2
3
4
try block
return statement
finally block
after return

咦?不是说finally块是在try或者catch中的return语句之前执行的的吗?return statement语句怎么会在finally block之前就打印了呢?

其实,第10行中的return test1();相当于下面这两条语句:

1
2
String temp = test1();
return temp;

也就是说,当执行到return test1();语句的时候:

  • 首先会执行String temp = test1();语句,则会先输出return statement
  • 然后再将返回值after return交给temp,其次再执行return temp;语句;
  • 但是在执行return temp;语句之前,会先执行finally中的finally block,所以最后输出的就是after return了。

经过上面的例子,你可能已经掌握了try-catch-finally的执行顺序,但且慢,来看最后两个小例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class ExceptionAndErrorTests {

    public static void main(String[] args) {
        System.out.println("return value of getValue(): " + getValue());
    }

    public static int getValue() {
        try {
            return 0;
        } finally {
            return 1;
        }
    }
}

输出结果如下:

1
return value of getValue(): 1

还有一个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class ExceptionAndErrorTests {

    public static void main(String[] args) {
        System.out.println("return value of getValue(): " + getValue());
    }

    public static int getValue() {
        int i = 1;
        try {
            return i;
        } finally {
            i++;
        }
    }
}

输出结果如下:

1
return value of getValue(): 1

按照之前的逻辑,最终的结果应该是2才对,为什么结果是1呢?

关于 Java 虚拟机是如何编译finally语句块的问题,有兴趣的可以参考《The JavaTM Virtual Machine Specification, Second Edition》中 3.13 节的 Compiling finally,那里详细介绍了 Java 虚拟机是如何编译 finally 语句块。

实际上,Java 虚拟机会把finally语句块作为 subroutine 直接插入到try语句块或者catch语句块的控制转移语句之前。但是,还有另外一个不可忽视的因素,那就是在执行 subroutine(也就是finally语句块)之前,try或者catch语句块会保留其返回值到 **本地变量表(Local Variable Table)**中。待 subroutine 执行完毕之后,再恢复保留的返回值到操作数栈中,然后通过return或者throw语句将其返回给该方法的调用者(invoker)。请注意,对于这条规则(保留返回值),只适用于returnthrow语句,不适用于breakcontinue语句,因为它们根本就没有返回值。

参考