从字节码角度带你彻底理解异常中catch,return和finally,再也不用死记硬背了

目录

try-catch字节码

多个catch的字节码指令分析

finally字节码分析

finally 中带return 

把finally中的return给去掉

开始两个例题的分析

先来看两道面试题:看会不会写,等讲解完相关的字节码知识点,最后还会再来分析这两道题的;

public class Test {
    public static void main(String[] args){
        int result = test2();
        System.out.println(result);
    }

    public static int test2(){
        int i = 1;
        try{
            i++;
            throw new Exception();
        }catch(Exception e){
            i--;
            System.out.println("catch block i = "+i);
        }finally{
            i = 10;
            System.out.println("finally block i = "+i);
        }
        return i;
    }
}

输出结果:

然后对刚刚到例题进行改动一下,来看看输出结果会是什么?

public class Test1 {
    public static void main(String[] args){
        int result = test3();
        System.out.println(result);
    }

    public static int test3(){
        int i = 1;
        try{
            i++;
            System.out.println("try block, i = "+i);
            return i;
        }catch(Exception e){
            i ++;
            System.out.println("catch block i = "+i);
            return i;
        }finally{
            i = 10;
            System.out.println("finally block i = "+i);
        }
    }
}

 

try-catch字节码

看一段简单的代码:

public class TryCatchTest {
    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        }catch (Exception e) {
            i = 20;
        }
    }
}

对应字节码指令分析:

 字节码指令:return 表示结束程序,ireturn才表示返回数据;

  • 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开(也就是检测 2~4 行)的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号

  • 第8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 2 号槽位

  • 异常表监测的范围内发生异常后,会直接跳转到异常表中target对应的字节码指令,中间的字节码指令不再执行,比如这个案例中的goto指令就没有执行

 

多个catch的字节码指令分析

 public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        }catch (ArithmeticException e) {
            i = 30;
        }catch () {
            i = 40;
        }catch(Exception e){
            i = 50;
        }
    }
Code:
      stack=1, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        10  
         4: istore_1
         5: goto          26 //通过异常表可以知道,发生异常跳到26行
         8: astore_2         //存储异常对象的引用
         9: bipush        30
        11: istore_1
        12: goto          26 //通过异常表可以知道,发生异常跳到26行
        15: astore_2         //存储异常对象的引用
        16: bipush        40
        18: istore_1
        19: goto          26 //通过异常表可以知道,发生异常跳到26行
        22: astore_2	     //存储异常对象的引用
        23: bipush        50
        25: istore_1
        26: return
      Exception table:
         from    to  target type   //这里的target和上面的行号对应
             2     5     8   Class java/lang/ArithmeticException
             2     5    15   Class java/lang/NullPointerException
             2     5    22   Class java/lang/Exception
 LineNumberTable....
      LocalVariableTable:   //Slot  多个变量使用同一个局部变量槽是为了复用,因为这些异常同一时刻只能发生一种,所以没必要创建多个槽位来存储异常对象,可以减少开销
        Start  Length  Slot  Name   Signature
            9       3     2     e   Ljava/lang/ArithmeticException;
           16       3     2     e   Ljava/lang/NullPointerException;
           23       3     2     e   Ljava/lang/Exception;
            0      27     0  args   [Ljava/lang/String;
            2      25     1     i   I

因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用,这样可以节省内存空间;

finally字节码分析

public static void main(String[] args) {
		int i = 0;
		try {
			i = 10;
		} catch (Exception e) {
			i = 20;
		} finally {
			i = 30;
		}
	}

对应字节码:

从下面字节码中我们可以看到,finally的作用是把finally中的代码快复制多分,然后分别放到try代码块后,catch代码块后(goto指令前),但是有时候catch并不能完全catch你想要的exception,所以这个字节码指令会多一个保障,就是在异常表中多捕获一个异常any,和对catch多捕获一个any的异常,下面的异常表中有;

Code:
     stack=1, locals=4, args_size=1
        0: iconst_0
        1: istore_1
        //try块
        2: bipush        10           //-----------try   try的范围可以从异常表中查询到
        4: istore_1
        //try块执行完后,会执行finally,即便try中发生了异常导致try中的finally指令无法执行,但是发生异常后会跳转到catch中,catch中还是有finally中的代码指令 的  
        5: bipush        30           //-----------fainal 中的i = 30
        7: istore_1					  // 把30赋值给i,覆盖局部变量表中的1号槽位的数据
        8: goto          27           //跳转到return指令,结束程序
       //catch块     
       11: astore_2 				  //把异常信息放入局部变量表的2号槽位
       12: bipush        20     	  //catch中的 i = 20代码
       14: istore_1					  //覆盖局部变量表中的1号槽位的数据
       //catch块执行完后,会执行finally          
       15: bipush        30           //-----------fainal 中的i = 30
       17: istore_1 
       18: goto          27           //跳转到return指令,结束程序             
       //出现异常,但未被Exception捕获,会抛出其他异常,这时也需要执行finally块中的代码   
       21: astore_3					  //存储其他类型的异常
       22: bipush        30           //-----------fainal 中的i = 30
       24: istore_1
       25: aload_3 				     //找到刚刚没有名字的异常
       26: athrow  				     //抛出这个没有名字的异常
       27: return
     Exception table:
        from    to  target type
            2     5    11   Class java/lang/Exception
            2     5    21   any      //any表示的是除了你要捕获的异常之外的异常
           11    15    21   any

可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程 注意:虽然从字节码指令看来,每个块中都有 finally 块,但是 finally 块中的代码只会被执行一次;

finally 中带return

先看一段代码:

public class FinallyReturnTest {
    public static void main(String[] args) {
        int i = FinallyReturnTest.test();
        // 结果为 20
        System.out.println(i);
    }
    public static int test() {
        int i;
        try {
            i = 10;
            return i;
        } finally {
            i = 20;
            return i;
        }
    }
}

对应字节码文件分析:

Code:
     stack=1, locals=3, args_size=0
        0: bipush        10   //放入栈顶
        2: istore_0           //slot 0  (从栈顶移除了,把该值存储到局部变量表中了)
        3: iload_0			  //从局部变量表中把0号槽位的数据加载到栈中
        4: istore_1           //注意:暂存返回值,又把这个10存储到局部变量表中的【1号】槽位 备份使用
        5: bipush        20   //------finally中代码块
        7: istore_0   		  //20这个值对0号槽位的10进行了覆盖
        8: iload_0			  //把0号槽位的值加载到栈中
        9: ireturn	          // ireturn 会【返回操作数栈顶】的整型值 20,返回的数据是操作数栈中的数据
       // 如果出现异常,还是会执行finally 块中的内容,没有抛出异常
       10: astore_2           // 存储异常,从异常表中得出该行指令是存储移除用的
       11: bipush        20   //------finally中代码块
       13: istore_0           //20这个值对0号槽位的10进行了覆盖
       14: iload_0   		  //把0号槽位的值加载到栈中
       15: ireturn	          //注意:这里没有 athrow 了,也就是如果在 finally 块中如果有返回操作的话,那么try中代码块出现异常,会吞掉异常!并不会抛出异常
     Exception table:
        from    to  target type
            0     5    10   any
  • 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以finally的为准

  • 至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子(有大用)

  • 跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常,如果try中也有return那么try中的return和finally中的return最终在字节码层面只会执行一条return指令

  • 所以不要在finally中进行返回操作

public static int test() {
      int i;
      try {
         i = 10;
         //  这里应该会抛出异常
         i = i/0;
         return i;
      } finally {
         i = 20;
         return i;
      }
   }

会发现打印结果为 20 ,而且并未抛出异常;

把finally中的return给去掉

 但是如果我们把finally中的return给去掉,那么返回的又是什么?

public static int test() {
		int i = 10;
		try {
			return i;
		} finally {
			i = 20;   //最后的结果是返回10  !!!
		}
	}

对应的字节码:

Code:
     stack=1, locals=3, args_size=0
        0: bipush        10  //把10放入栈顶
        2: istore_0          // 把10存储在局部变量表的0号槽位
        3: iload_0	         // 然后从局部变量表中把10又加载到操作数栈顶,按理说此时该返回了,但是明显没有立马返回,而是istore_1,把刚刚加载到操作数栈中的10又在局部变量表中的1号槽位备份一份
        4: istore_1          // 加载到局部变量表的1号位置,【目的是为了固定返回值】
        5: bipush        20  //------执行finally代码块
        7: istore_0          // 把20赋值给i
        8: iload_1           // 【加载局部变量表1号位置的数10到操作数栈】
        9: ireturn           // 返回操作数栈顶元素 10
       10: astore_2          //存储异常对象
       11: bipush        20  //------执行finally代码块
       13: istore_0          //把20赋值给i
       14: aload_2           // 加载异常
       15: athrow            // 仍然会抛出异常
     Exception table:
        from    to  target type
            3     5    10   any

在把finally中的return去掉后,我们发现如果在try中进行了return, 如果没有发生异常的话,那么即便finally中的变量发生了变化,那么try中返回的依旧是try中的变量值,因为我们可以从字节码指令看到try中的变量会先被备份一次用来返回;

如果发生了异常那么返回的值就是catch中的变量:比如下面的案例;

字节码分析: 

开始两个例题的分析

这里就不再使用字节码码来分析了,前面已经把try  catch  finally对应的字节码都分析了一遍,所以这里就不再使用字节码来分析了;

public class Test {
    public static void main(String[] args){
        int result = test2();
        System.out.println(result);
    }

    public static int test2(){
        int i = 1;
        try{
            i++;   //i从自增变成2
            throw new Exception();  //抛出异常,被catch捕获
        }catch(Exception e){
            i--;  //对i进行自减操作  变为1
            System.out.println("catch block i = "+i);   //执行这行代码  此时i为1
      //finally中的代码被拷贝到这里来执行  【i = 10 把之前的i进行覆盖 ,此时局部变量表中存储的i为10】
      //输出System.out.println("finally block i = "+10);
        }finally{
            i = 10;
            System.out.println("finally block i = "+i);
        }
        return i;  //返回i,先从局部变量表中加载对应的变量,然后进行弹栈
    }
}

所以输出的是:

catch block i = 1
finally block i = 10
10

第二题:

public class Test1 {
    public static void main(String[] args){
        int result = test3();
        System.out.println(result);
    }

    public static int test3(){
        int i = 1;
        try{
            i++;  //i从自增变成2,然后存储到局部变量表中
            System.out.println("try block, i = "+i);  //输出
        //因为没有发生异常所以不会跳转到catch中,但是finally中的代码会被拷贝在return之前来执行
        //因为finally中没有return,所以try和catch中的变量都会自己额外拷贝一份,用来最后作为返回值返回
            return i;
        }catch(Exception e){
            i ++;
            System.out.println("catch block i = "+i);
            return i;
        }finally{
            i = 10;
            System.out.println("finally block i = "+i);
        }
    }
}

输出结果:

try block, i = 2
finally block i = 10
2

字节码片段分析:如果finally中没有return,那么try 和catch中都是会对直接到 i 进行备份用于返回的,即便finally中的代码改变了其值,但是最后返回的值还是以try和catch中的值为准,因为JVM返回的是备份的值;

猜你喜欢

转载自blog.csdn.net/weixin_53142722/article/details/125467334