try-catch-finally字节码实例探究

本文使用Idea的jclasslib插件查看字节码。本文全程自言自语,请勿自行代入。

概述

java是怎么处理try-catch-finally的?

我们在深入理解java虚拟机第三版读书笔记06学习过,在class字节码中的属性表中,存在code属性,它用于存放各方法的字节码、属性等。其中就包括exception_info,即异常表,它记录了当一个异常发生后,应跳转到哪一行继续执行,而finally里面的代码块编译成字节码后则是被复制成许多份分别附在try和各catch块的后面。这篇文章,我们就通过一些实例来看看,java中的try-catch-finally引申出的一些问题。

普通的例子

普通中的普通例子

我们先来一个普通的例子

public static void normal(){
    try {
        System.out.print("try-");
    }catch (Exception e){
        System.out.print("catch");
    }finally {
        System.out.print("finally");
    }
}

这个相信大家都知道是输出try-finally,因为没有异常出现,那么它的字节码是:

 0 getstatic #4 <java/lang/System.out>
 3 ldc #5 <try->
 5 invokevirtual #6 <java/io/PrintStream.print>
---------------- 上面这部分输出"try-"-----------------
 8 getstatic #4 <java/lang/System.out>
11 ldc #7 <finally>
13 invokevirtual #6 <java/io/PrintStream.print>
---------------- 上面这部分输出"finally"--------------
16 goto 50 (+34)
-----------正常流程,跳转到50行直接return--------------
19 astore_0
20 getstatic #4 <java/lang/System.out>
23 ldc #8 <catch>
25 invokevirtual #6 <java/io/PrintStream.print>
-----上面这部分输出"catch",astore_0存入的是异常信息-----
28 getstatic #4 <java/lang/System.out>
31 ldc #7 <finally>
33 invokevirtual #6 <java/io/PrintStream.print>
36 goto 50 (+14)
-----注意上面这部分也是输出"finally",和之前某部分一样----
39 astore_1
40 getstatic #4 <java/lang/System.out>
43 ldc #7 <finally>
45 invokevirtual #6 <java/io/PrintStream.print>
48 aload_1
49 athrow
-----注意上面这部分也是输出"finally",但抛出了一个异常----
50 return

我们看看异常表:

try-catch-finally字节码实例探究

如何解读:
+ 0-8行(不包括8)的语句出现异常(类型为Exception,是我们catch的异常类型),交由19行处理(catch代码块),我们注意到28行执行完catch代码块后,就执行finally代码块。
+ 0-8行的语句出现异常(类型为any,代表任何异常),交由39行处理,为什么这里会有这一项呢?是因为我们定义了finally,如果在try中出现了我们没有catch的异常类型,就可能会出现finally中的代码没有执行的情况,所以为了防止finally中的代码不执行,编译器会自动为我们暂时store这个异常信息(对应39行),执行完finally语句块后再抛出(对应49行)
+ 19-28行的语句出现异常(类型为any,代表任何异常),交由39行处理。为什么这里会有这一项呢?原因和上面一样,如果执行catch代码块又出现了异常,也要保证finally代码块的执行。

普通例子举一反三

有了上面这个例子,我们就来思考一下,如果没有finally代码块,字节码会是什么样子的呢?

public static void normal(){
    try {
        System.out.print("try-");
    }catch (Exception e){
        System.out.print("catch");
    }
}

首先能想到的是,正常流程执行完try中的内容就应该直接返回。而出现异常就应该跳转至中间一段用于处理异常的代码块。因为没有finally代码块,应该也不会有那么多冗余代码块被复制。

验证:

 0 getstatic #4 <java/lang/System.out>
 3 ldc #5 <try->
 5 invokevirtual #6 <java/io/PrintStream.print>
----------上面输出"try-"-------------------------
 8 goto 20 (+12)
----------正常流程跳转至20行之间返回--------------
11 astore_0
12 getstatic #4 <java/lang/System.out>
15 ldc #7 <catch>
17 invokevirtual #6 <java/io/PrintStream.print>
------上面输出"catch",是catch代码块的内容---------
20 return

这么一看确实清爽多了,再看看异常表:

try-catch-finally字节码实例探究

确实异常表也清爽多了,因为不必保证finally代码块的执行,这里只有我们显式catch的一种异常。

没事自己抛出异常玩玩,会发生什么?

try抛出异常

public static void throwAndCatch(){
    try {
        throw new Exception();
        System.out.println("骚一下");
    }catch (Exception e){
        System.out.println("抓住了");
    }
    System.out.println("我在外面");
}

这段代码的原型来自牛客网一道题,输出是什么?

答案应该是抓住了我在外面,catch是可以捕获在try中手动抛出的异常的,但因为部分编译器优化程度过高知道System.out.println("骚一下");是不可达语句,会报编译错误。我没找到怎么关闭这个异常检查 TT_TT

catch抛出异常

catch中抛出异常的情况,我们把上面那段代码改成:

public static void throwAndCatch(){
    try {
        int i = 1/0;
        System.out.println("出现异常了");
    }catch (Exception e){
        System.out.println("抓住了");
        throw new RuntimeException();
    }
    System.out.println("我在外面");
}

为什么要抛RuntimeException?因为RuntimeException是非受查异常,可以不用catch,否则即使是在catch中抛出异常还是需要再嵌套一层catch。

那么这段代码会输出什么?我相信这里可能就有人猜不准了,正确答案应该是抓住了,并接着抛出运行时异常。

来看一下字节码:

 0 iconst_1
 1 iconst_0
 2 idiv
 3 istore_0
---------上面这段代码做1/0的除法--------------------
 4 getstatic #4 <java/lang/System.out>
 7 ldc #8 <出现异常了>
 9 invokevirtual #9 <java/io/PrintStream.println>
----------上面输出"出现异常了"----------------------
12 goto 32 (+20)
-------正常流程跳转32行,执行外面的代码--------------
15 astore_0
16 getstatic #4 <java/lang/System.out>
19 ldc #10 <抓住了>
21 invokevirtual #9 <java/io/PrintStream.println>
----------上面输出"抓住了"----------------------
24 new #11 <java/lang/RuntimeException>
27 dup
28 invokespecial #12 <java/lang/RuntimeException.<init>>
31 athrow
----------上面抛出运行时异常----------------------
32 getstatic #4 <java/lang/System.out>
35 ldc #13 <我在外面>
37 invokevirtual #9 <java/io/PrintStream.println>
----------上面输出"我在外面"----------------------
40 return

来看看异常表:

try-catch-finally字节码实例探究

显然这里也只有我们显式catch的Exception处理。那我们来分析一下执行流程:

  • 0-3行,执行1/0的除法,出现了除以零的异常,因为此时行号处于0-12之间,就交给15行继续处理,所以"出现异常了"没有输出。
  • 15-21行,输出"抓住了",继续执行。
  • 24-31行,new了一个RuntimeException,并抛出,因为没有该异常没有对应的处理,程序终止,后面的"我在外面"也不会输出。

举一反三

public static void throwAndCatch(){
    try {
        int i = 1/0;
        System.out.println("出现异常了");
    }catch (Exception e){
        System.out.println("抓住了");
        throw new RuntimeException();
    }finally {
        System.out.println("必须执行");
    }
    System.out.println("我在外面");
}

来看看这段代码,增加了一个finally语句块会输出什么呢?这里就不研究字节码了,因为原因之前在普通例子的时候就提到过了,如果catch代码块中出现异常,并且存在finally代码块,会先把异常信息存起来,执行完finally之后再抛出异常。

那么相信你也知道了,这里的输出应该是:抓住了必须执行

我都要return了,finally还会执行吗?

普通例子

public static int var(){
    int i;
    try {
        i = 10;
        System.out.println("修改成10了哦");
        return i;
    }catch (Exception e){
        i = 20;
        System.out.println("修改成20了哦");
    }finally {
        i = 30;
        System.out.println("修改成30了哦");
    }
    return i;
}

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

这个例子开始有一点难度了,你能告诉我它会输出什么吗?

首先catch内的代码是不会执行的,因为没有异常抛出,你觉得finally中的代码会执行吗?

答案是会的,看看输出:

修改成10了哦
修改成30了哦
10

是否会大吃一惊?finally中的代码执行了,应该也将i修改为30了,怎么输出的还是10?

看看字节码:

 0 bipush 10
 2 istore_0
----------上面将10存储到局部变量表----------------
 3 getstatic #2 <java/lang/System.out>
 6 ldc #16 <修改成10了哦>
 8 invokevirtual #10 <java/io/PrintStream.println>
----------上面输出"修改成10了哦"------------------
11 iload_0
12 istore_1
-----上面将10先读出,又存储到局部变量表的1号槽位-----
13 bipush 30
15 istore_0
------上面将30存储到局部变量表的0号槽位------------
16 getstatic #2 <java/lang/System.out>
19 ldc #17 <修改成30了哦>
21 invokevirtual #10 <java/io/PrintStream.println>
----------上面输出"修改成30了哦"------------------
24 iload_1
25 ireturn
----------从一号槽位读出数据,并返回---------------
26 astore_1
27 bipush 20
29 istore_0
------上面将20存储到局部变量表的0号槽位------------
30 getstatic #2 <java/lang/System.out>
33 ldc #18 <修改成20了哦>
35 invokevirtual #10 <java/io/PrintStream.println>
----------上面输出"修改成20了哦"------------------
38 bipush 30
40 istore_0
41 getstatic #2 <java/lang/System.out>
44 ldc #17 <修改成30了哦>
46 invokevirtual #10 <java/io/PrintStream.println>
----这段和之前看过的一段一样,是finally块会干的事-----
49 goto 66 (+17)
52 astore_2
53 bipush 30
55 istore_0
56 getstatic #2 <java/lang/System.out>
59 ldc #17 <修改成30了哦>
61 invokevirtual #10 <java/io/PrintStream.println>
64 aload_2
65 athrow
----这段和之前看过的一段一样,是finally块会干的事-----
66 iload_0
67 ireturn
----------从零号槽位读出数据,并返回---------------

这段字节码是不是看着头疼,其实只需要注意到它在频繁地操作局部变量表就行了。其他流程和我们在普通例子里介绍的差不多。

看看异常表:

try-catch-finally字节码实例探究

嗯。。确实跟我们之前介绍的普通例子差不多,那么为什么会出现这种奇异输出呢?我们来分析一下执行流程:

  • 0-2行,将10存储于局部变量表0号槽。
  • 3-8行输出"修改成10了哦"
  • 11-15行,将10转移到局部变量表1号槽,将30存入0号槽
  • 19-21行,输出"修改成30了哦"
  • 24-25行,从1号槽中读出10,并返回。

牛的!其实执行的流程就这么点,注意到finally执行前后,程序分别将10存入局部变量表和从局部变量表中取出,实现了finally中对变量的修改不影响即将return的结果。

为什么会出现这种情况,因为编译器知道try中即将return数据,但是finally还是得执行,所以为了保护即将返回的数据,会先将该数据存起来,等finally执行完再取出并返回。

举一反三

public static int var(){
    int i;
    try {
        i = 10;
        System.out.println("修改成10了哦");
        int a = 1/0;
        return i;
    }catch (Exception e){
        i = 20;
        System.out.println("修改成20了哦");
        return i;
    }finally {
        i = 30;
        System.out.println("修改成30了哦");
    }
}

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

这个例子稍微更复杂点了,执行到catch中去了,那么输出是什么呢?

如果你有好好得跟着我看下来,应该不难猜到,try中异常前的语句、catch和finally中得语句都会执行,实际上,finally也不会影响catch返回的结果:

修改成10了哦
修改成20了哦
修改成30了哦
20

抢着return,谁优先?

普通例子

public static int num(){
    try {
        return 10;
    }catch (Exception e){
        return 20;
    }finally {
        return 30;
    }
}

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

这个例子不复杂吧,经过了这么多历练,你能猜到结果吗?

答案其实是30,注意finally中如果出现了return语句,由于finally必会执行,总是finally中的return语句生效。

字节码也不难:

 0 bipush 10
 2 istore_0
 3 bipush 30
 5 ireturn
-----在此处就执行了finally并返回-----
 6 astore_0
 7 bipush 20
 9 istore_1
10 bipush 30
12 ireturn
13 astore_2
14 bipush 30
16 ireturn

举一反三

稍微修改一下上面的例子:

public static int num(){
    try {
        int i = 1/0;
        return 10;
    }catch (Exception e){
        return 20;
    }finally {
        return 30;
    }
}

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

输出是什么?如果你能很快给出答案,我觉得你对try-catch-finally的理解已经差不多了

有人可能要问了,学习这些是为了什么呢?谁写代码会这么写呢?那不都是为了应付面试吗?

finally不会执行的例外情况

有人看了全文就要说了,懂了!反正finally必执行,记住这个就万无一失了!

其实finally也不是那么的保险,比如我给你举个例子:

try{
    System.exit(1);
}finally{
    System.out.println("finally执行了");
}

这段代码中,finally就不会执行了,遇到System.exit,程序会直接退出,相当于杀进程了。

其他情况例如你外部杀进程,断电脑电源(手动滑稽),finally也是不会执行的。

OK,那么这篇文章就写到这了,长篇原创好文希望大家支持(^_^)

原创文章,作者:彭晨涛,如若转载,请注明出处:https://www.codetool.top/article/try-catch-finally%e5%ad%97%e8%8a%82%e7%a0%81%e5%ae%9e%e4%be%8b%e6%8e%a2%e7%a9%b6/