引言

上一篇文章,我们学习了java中异常的分类和编译时检查,编译时检查由编译器在编译期间执行,针对的是受检异常。在程序运行期间,java有自己的异常处理机制,这篇文章我们就来重点看一下运行时的异常处理。
首先声明,这里主要根据《java语言规范》第11章中的内容进行讲解,读者可以参考这些章节。

异常的运行时处理的含义

在程序运行过程中,如果某个语句抛出了异常,java就会启动相应的机制来处理发生的异常。我们在编码过程中,可以使用try catch finally来控制异常的处理。下面,我们就来看一下在不同的组合(try-catch,try-catch-finally,try-finally)中,异常的处理过程是怎样的。

try语句

try语句会执行一个语句块。如果抛出了值并且try语句有一个或者多个可以捕获它的catch子句,那么控制流就会转移到第一个这种catch子句上。如果try语句有finally子句,那么将会执行另一个代码块,无论try块是正常结束还是猝然结束,并且无论控制流之前是否转移到某个catch子句上。

try-catch语句

不带finally的try语句是由先执行try块而执行的。然后有以下选择:

  • 如果try块的执行正常结束,那么就不会有任何更近一步的动作,并且该try语句正常结束。
  • 如果try块的执行因一个值V的throw对象而猝然结束,那么有以下选择:
    • 如果V的运行时类型与try语句的任何catch子句的可捕获异常类是赋值兼容的,那么第一个的这种catch子句将被选中。值V被赋值给选中的catch子句的参数,并且该catch子句的语句块将被执行,然后有以下两种情况:
      • 如果该块正常结束,那么该try语句正常结束。
      • 如果该块因某项原因而猝然结束,那么该try语句会以同样的原因而猝然结束。
    • 如果v的运行时类型与try语句的任何catch子句的可捕获异常类都不是赋值兼容的,那么该try语句就会因一个V值的throw对象而猝然结束。
  • 如果try块的执行因某项原因而猝然结束,那么该try语句就会以同样的原因而猝然结束。

下面几个例子分别演示了这几种情况。

try块中的语句正常结束。

  1. public static void main(String[] args) {
  2. int i =10;
  3. try {
  4. i = i + 100;
  5. System.out.println("在try块内执行了加法操作后i="+i);
  6. }catch (Exception e){
  7. System.out.println("在catch块内发生了异常");
  8. }
  9. System.out.println("在try-catch块之外的语句被执行了");
  10. }

输出如下:

  1. try块内执行了加法操作后i=110
  2. try-catch块之外的语句被执行了

在这个示例里面,我们使用了try-catch语句,并且try块中的语句正常运行,没有猝然结束,所以catch块内的语句不会被执行,程序继续运行到try-catch语句范围外。

try块中抛出异常并在catch块中被捕获

  1. public static void main(String[] args) {
  2. int i = 10;
  3. try {
  4. i = i + 100;
  5. throw new RuntimeException();
  6. }catch (Exception e){
  7. System.out.println("catch块中捕获到了异常");
  8. }
  9. System.out.println("在try-catch块之外的语句被执行了");
  10. }

输出如下:

  1. catch块中捕获到了异常
  2. try-catch块之外的语句被执行了

异常在catch块中被正常处理,并且catch块正常结束,这样会继续运行try-catch块外的程序。

try块中抛出异常并且有多个catch块

  1. public static void main(String[] args) {
  2. int i = 10;
  3. try {
  4. i = i + 100;
  5. throw new RuntimeException();
  6. }catch (IllegalArgumentException e){
  7. System.out.println("IllegalArgumentException块中捕获到了异常");
  8. }catch (RuntimeException e){
  9. System.out.println("RuntimeException中捕获到了异常");
  10. }
  11. System.out.println("在try-catch块之外的语句被执行了");
  12. }

输出结果如下:

  1. RuntimeException中捕获到了异常
  2. try-catch块之外的语句被执行了

这两个catch子句中,一个捕获的是IllegalArgumentException,第二个捕获的是RuntimeException,而try块中抛出的是RuntimeException,所以会被后者捕获。程序同样会运行到try-catch外的范围。

所有的catch块都没有捕获该异常

  1. public static void main(String[] args) {
  2. int i = 10;
  3. try {
  4. i = i + 100;
  5. throw new IllegalStateException();
  6. }catch (IllegalArgumentException e){
  7. i += 200;
  8. System.out.println("IllegalArgumentException块中捕获到了异常");
  9. }catch (IllegalReceiveException e){
  10. i += 300;
  11. System.out.println("IllegalReceiveException中捕获到了异常");
  12. }
  13. System.out.println(i);
  14. System.out.println("在try-catch块之外的语句被执行了");
  15. }

输出结果如下:

  1. Exception in thread "main" java.lang.IllegalStateException
  2. at person.andy.concurrency.exception.TryCatchTest.main(TryCatchTest.java:11)

try块中抛出的是IllegalStateException,但是两个catch语句中分别捕获的是IllegalArgumentException和IllegalReceiveException异常,都不是IllegalStateException的超类,所以异常没有被捕获,导致try语句猝然结束。try-catch范围外的程序不会被执行。

捕获异常的try块中抛出了异常

  1. public static void main(String[] args) {
  2. int i = 10;
  3. try {
  4. i = i + 100;
  5. throw new IllegalStateException();
  6. }catch (IllegalStateException e){
  7. i += 200;
  8. System.out.println("IllegalArgumentException块中捕获到了异常");
  9. throw new IllegalArgumentException();
  10. }
  11. }

这个例子中,try块中抛出的IllegalStateException异常被catch块捕获,但是catch块紧接着又把这个异常抛出去了,那同样会导致try语句的猝然结束,try-catch范围外的程序不会被执行。

try-finally语句和try-catch-finally语句

带有finally块的try语句是由先执行try块而执行的。然后有以下选择:

  • 如果try块的执行正常结束,那么finally块就会被执行:
    • 如果finally块正常结束,那么try语句也正常结束。
    • 如果finally块因原因S猝然结束,那么该try语句也会因S而猝然结束。
  • 如果try块的执行因一个V值的throw对象而猝然结束:
    • 如果V的运行时类型与try语句的任何catch子句的可捕获异常类是赋值兼容的,那么第一个(最左边)的这种catch子句将被选中。值V被赋值给选中的catch子句的参数,并且该catch子句的语句块将被执行,然后有以下选择:
      • 如果catch块正常结束,那么finally块就会被执行。然后有以下两种情况:
        • 如果finally块正常结束,那么try语句也正常结束。
        • 如果finally块因某项原因猝然结束,那么该try语句也会以同样的原因而猝然结束。
      • 如果catch块因原因R猝然结束,那么finally块就会被执行。然后有以下两种情况:
        • 如果finally块正常结束,那么该try语句与就会以原因R而猝然结束。
        • 如果finally块因原因S猝然结束,那么该try语句就会因原因S而猝然结束(并且原因R会被舍弃)。
    • 如果V的运行时类型与try语句的任何catch子句的可捕获异常类都不是赋值兼容的,那么finally块就会被执行。然后有以下选择:
      • 如果finally块正常结束,那么该try语句就会因一个V值的throw对象而猝然结束。
      • 如果finally块印原因S猝然结束,那么该try语句就会因原因S而猝然结束(并且V值的throw对象会被丢弃和忘记)。
  • 如果try的执行因为其他原因R而猝然结束,那么finally块将被执行:
    • 如果finally块正常结束,那么该try语句就会因原因R而猝然结束。
    • 如果finally块因原因S猝然结束,那么该try语句就会因原因S而猝然结束(并且原因R会被丢弃)。

      try正常结束,finally正常结束

      1. public static void main(String[] args) {
      2. int i = 9;
      3. try {
      4. i = i + 10;
      5. }catch (Exception e){
      6. System.out.println("catch块中捕获到了异常");
      7. }finally {
      8. System.out.println("finally被执行");
      9. }
      10. System.out.println("try-catch-finally外的代码被执行");
      11. }

输出如下:

  1. finally被执行
  2. try-catch-finally外的代码被执行

try正常结束,finally猝然结束

image.png

可以看到finally中显式抛出了一个RuntimeException,try-catch-finally外的代码被idea标识为不可达的语句,也就是说,try正常结束,finally猝然结束的情况下,try-catch-finally外的代码不必被执行。

try猝然结束,catch成功捕获并正常结束,finally正常结束

  1. public static void main(String[] args) {
  2. int i = 9;
  3. try {
  4. i = i + 10;
  5. throw new RuntimeException();
  6. }catch (RuntimeException e){
  7. System.out.println("catch块中捕获到了异常");
  8. }finally {
  9. System.out.println("finally被执行");
  10. }
  11. System.out.println("try-catch-finally范围外的代码被执行");
  12. }

输出结果如下:

  1. catch块中捕获到了异常
  2. finally被执行
  3. try-catch-finally范围外的代码被执行

try猝然结束,catch成功捕获并正常结束,finally猝然结束

  1. public static void main(String[] args) {
  2. int i = 9;
  3. try {
  4. i = i + 10;
  5. throw new RuntimeException();
  6. }catch (RuntimeException e){
  7. System.out.println("catch块中捕获到了异常");
  8. }finally {
  9. System.out.println("finally被执行");
  10. int j = 100/0;
  11. }
  12. System.out.println("try-catch-finally范围外的代码被执行");
  13. }

在这段代码里面,try块中抛出了异常并且在catch内被捕获,但是finally块进行了除以0的操作,会抛出异常,输出结果如下:

  1. catch块中捕获到了异常
  2. finally被执行
  3. Exception in thread "main" java.lang.ArithmeticException: / by zero
  4. at person.andy.concurrency.exception.TryCatchFinallyTest.main(TryCatchFinallyTest.java:13)

可以看到finally猝然结束,try-catch-finally范围外的代码不会被执行。

try猝然结束,catch成功捕获但猝然结束,finally正常结束

  1. public static void main(String[] args) {
  2. int i = 9;
  3. try {
  4. i = i + 10;
  5. throw new RuntimeException();
  6. }catch (RuntimeException e){
  7. System.out.println("catch块中捕获到了异常");
  8. int j = 100/0;
  9. }finally {
  10. System.out.println("finally被执行");
  11. }
  12. System.out.println("try-catch-finally范围外的代码被执行");
  13. }


在这个例子中,try中抛出的异常被catch捕获,但是catch中进行了除以0的操作,抛出了异常,转为finally处理,finally正常结束,但是由于catch中发生了异常,就不会执行try-catch-finally外的代码。
输出如下:

  1. catch块中捕获到了异常
  2. finally被执行
  3. Exception in thread "main" java.lang.ArithmeticException: / by zero
  4. at person.andy.concurrency.exception.TryCatchFinallyTest.main(TryCatchFinallyTest.java:11)

try猝然结束,catch成功捕获但猝然结束,finally猝然结束

  1. public static void main(String[] args) {
  2. int i = 9;
  3. try {
  4. i = i + 10;
  5. throw new RuntimeException();
  6. }catch (RuntimeException e){
  7. System.out.println("catch块中捕获到了异常");
  8. int j = 100/0;
  9. }finally {
  10. int k = 100/0;
  11. System.out.println("finally被执行");
  12. }
  13. System.out.println("try-catch-finally范围外的代码被执行");
  14. }

try中抛出了异常,在catch中被捕获,但是catch由于进行了除以0的操作,抛出了异常,转到finally中进行处理,处理过程中同样抛出了异常,try-catch-finally范围外的代码不会被执行。
输出结果如下:

  1. catch块中捕获到了异常
  2. Exception in thread "main" java.lang.ArithmeticException: / by zero
  3. at person.andy.concurrency.exception.TryCatchFinallyTest.main(TryCatchFinallyTest.java:13)

try猝然结束,catch未捕获,finally正常结束

  1. public static void main(String[] args) {
  2. int i = 9;
  3. try {
  4. i = i + 10;
  5. throw new RuntimeException();
  6. }catch (IllegalArgumentException e){
  7. System.out.println("catch块中捕获到了异常");
  8. int j = 100/0;
  9. }finally {
  10. System.out.println("finally被执行");
  11. }
  12. System.out.println("try-catch-finally范围外的代码被执行");
  13. }

这个例子中,try块中抛出了RuntimeException异常,catch语句中捕获的是IllegalArgumentException,因此没有捕获到,转到finally执行,finally正常结束,但是由于try中的异常,导致try-catch-finally外的代码不会执行。
输出结果如下:

  1. finally被执行
  2. Exception in thread "main" java.lang.RuntimeException
  3. at person.andy.concurrency.exception.TryCatchFinallyTest.main(TryCatchFinallyTest.java:8)

try猝然结束,catch未捕获,finally猝然结束

  1. public static void main(String[] args) {
  2. int i = 9;
  3. try {
  4. i = i + 10;
  5. throw new RuntimeException();
  6. }catch (IllegalArgumentException e){
  7. System.out.println("catch块中捕获到了异常");
  8. }finally {
  9. System.out.println("finally被执行");
  10. int j = 100/0;
  11. }
  12. System.out.println("try-catch-finally范围外的代码被执行");
  13. }

在这个例子中,try中抛出了RuntimeException异常,catch语句中捕获的是IllegalArgumentException异常,因此没有捕获到,转到finally运行,finally进行了除以0的操作抛出了异常,try-catch-finally外的代码不会被运行。

  1. finally被执行
  2. Exception in thread "main" java.lang.ArithmeticException: / by zero
  3. at person.andy.concurrency.exception.TryCatchFinallyTest.main(TryCatchFinallyTest.java:13)

带有return的情况

try中带有return

首先看下面的例子:

  1. private static int tryReturn(){
  2. int i = 1;
  3. try{
  4. i ++ ;
  5. System.out.println("try:" + i);
  6. return i;
  7. }catch (Exception e){
  8. i ++;
  9. System.out.println("catch:" + i);
  10. }finally {
  11. i ++;
  12. System.out.println("finally:" + i);
  13. }
  14. return i;
  15. }
  16. public static void main(String[] args) {
  17. System.out.println(tryReturn());
  18. }

代码在try块中没有异常,通过return来返回结果,但是由于finally是必须执行的,那么这时候的执行顺序是怎样的呢?首先看输出的结果:

  1. try:2
  2. finally:3
  3. 2

首先解释一下:当try中带有return时,会先执行return前的代码,然后暂时保存需要return的信息,再执行finally中的代码,然后再通过return返回之前保存的信息。这里方法返回的值是try中计算之后的2而不是finally中计算后的3。但是有一点需要注意,看下面的例子:

  1. private static InnerClass tryReturn(){
  2. InnerClass innerClass = new InnerClass();
  3. try{
  4. innerClass.name = "try";
  5. return innerClass;
  6. }catch (Exception e){
  7. innerClass.name = "catch";
  8. }finally {
  9. innerClass.name = "finally";
  10. }
  11. return innerClass;
  12. }
  13. public static void main(String[] args) {
  14. System.out.println(tryReturn().name);
  15. }
  16. private static class InnerClass{
  17. public String name;
  18. }

这次输出的结果是:finally。
这是因为InnerClass是引用类型而不是简单类型,返回的使指向堆上对象实例的引用。而堆上的实例指向的name已经被修改。所以最后输出的是finally。

catch中带有return

  1. private static int catchReturn(){
  2. int i = 1;
  3. try {
  4. i++;
  5. System.out.println("try:"+i);
  6. i = i/0;
  7. }catch (Exception e){
  8. i ++;
  9. System.out.println("catch:"+i);
  10. return i;
  11. }finally {
  12. i ++;
  13. System.out.println("finally:"+i);
  14. }
  15. return i;
  16. }
  17. public static void main(String[] args) {
  18. System.out.println(catchReturn());
  19. }

输出结果是:

  1. try:2
  2. catch:3
  3. finally:4
  4. 3

catch中的return和try中一样,会先执行return前的代码,然后暂时保存需要return的信息。所以,这里返回的是try、catch中累计计算后的3,而非finally中计算后的4。

finally中带有return

  1. private static int finallyReturn() {
  2. int i = 1;
  3. try {
  4. i++;
  5. System.out.println("try:" + i);
  6. return i;
  7. } catch (Exception e) {
  8. i++;
  9. System.out.println("catch:" + i);
  10. return i;
  11. } finally {
  12. i++;
  13. System.out.println("finally:" + i);
  14. return i;
  15. }
  16. }
  17. public static void main(String[] args) {
  18. System.out.println(finallyReturn());
  19. }

输出结果为:

  1. try:2
  2. finally:3
  3. 3

当finally中有return的时候,try中的return会失效,在执行完finally的return之后,就不会再执行try中的return。这种写法,编译是可以通过的,但是编译器会给出警告,所以不推荐finally中写return。
总结如下:
(1)finally中的代码总会被执行。
(2)当try、catch中有return时,也会执行finally。return的时候,要注意返回值的类型,是否受到finally中代码的影响。
(3)finally中有return时,会直接在finally中退出,导致try、catch中的return失败。

小结

异常的运行时处理可能比较繁琐,涉及到很多种情况并且每种情况下又可能有多种情况。这些不需要死记硬背,我们还是要理解异常处理的基本思想。