Java之异常处理原理

190 阅读6分钟

异常概念(是什么)

异常,从字面理解就是只不是我们预期内所期望的,也就是说是我们预料之外,并且会阻碍当前方法继续执行的一种问题。接下来,我们看一下他的关系是怎么样的。(由于比较多,就选择了我们可能会经常遇到的)

Exception.png
这是我使用IDEA打印出来的一个关系图。从图中可以看出,异常的顶级接口是Throwable,在下面还有一个Error子类,这个是意味着错误,而不是异常。异常是程序员可以进行修改代码后,能够保证程序的正常运行,错误是程序员不可控的一个情况。

异常分类

我们可以将异常分为两个大类

  • 可检查异常 可检查异常即在编码的时候,程序员便已经知道可能会出现什么异常,需要程序员进行处理。在上图中,SQLException就是一个典型的受检查异常,其父类是Exception.
  • 运行时异常 运行时异常是指程序在运行时,由于程序的结构或者是程序员的疏忽等原因导致的程序发生未知问题的情况,在编码时是未知的,只有在程序运行时才会发现。(当然,此处的编码时未知并不是指不能发现,而是我们当时没有预料到他会抛出异常)。对于这类异常我们可以通过代码走查等方式发现并解决,尽量减少即可。在上图中RuntimeException是所有运行时异常的父类,NullPointerException是很典型的一个运行时异常,在编译时无法发现这一类异常信息。

原理

try-catch

中断程序的执行,或者当发生异常的时候,按照程序员的设定,执行代码,避免因为脏数据的出现,导致程序崩溃。我们来看一段代码

public class Main {

    public static void main(String[] args) {
        Main main = new Main();
        System.out.println(main.div());
    }

    public int div() {
        int i = 1 / 0;
        System.out.println(i);
        return i;
    }

}

这一段代码在编译时没有任何问题,但是在运行的时候就会出错。

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at cn.pzhu.impassive.Main.div(Main.java:12)
	at cn.pzhu.impassive.Main.main(Main.java:7)

就给出了一个算术异常,我们也没有收到返回值。如果我们想让程序继续执行,就需要对这个异常进行处理。(如果出异常返回-1)

public class Main {

    public static void main(String[] args) {
        Main main = new Main();
        System.out.println(main.div());
    }

    public int div() {
        int i;
        try {
            i = 1 / 0;
        } catch (ArithmeticException e) {
            System.out.println("我是异常,我被捕捉了");
            i = -1;
        }
        return i;
    }
}

这样通过catch对异常进行捕捉即可,然后进行处理(空也是一种空处理)。这里提到catch捕捉异常是通过catch列表由上往下进行匹配,直到匹配成功,即退出匹配。所以,尽量把范围小的异常写在前面,这样打印异常信息的时候才可以更清晰。

try-catch-finally

上面是一种处理方式,但是存在一个问题。如果是资源类,如FileReader等出现了异常,需要释放资源,但是抛出异常后,抛出点后面的代码都不会执行,也就无法释放资源,当然,也可以在catch中进行释放,但是这样的话,又有重复代码。所以出现了finally。使用如下

public class Main {

    public static void main(String[] args) {
        Main main = new Main();
        System.out.println(main.div());
    }

    public int div() {
        int i;
        try {
            i = 1 / 0;
            return i;
        } catch (ArithmeticException e) {
            System.out.println("我是异常,我被捕捉了");
            i = -1;
        } finally {
            i = 2;
        }
        return i;
    }
}

这个的返回值是2。finally代码苦熬不论是否出现异常,都出执行,我们来看一下编译后的代码。(这里我只摘取了div方法的相关代码)

 public int div();
    descriptor: ()I
    flags: ACC_PUBLIC   # 标志,代表方法的访问属性为public,可以有其他的参数,这里不详解
    Code:  # 代码块
      stack=2, locals=4, args_size=1
         0: iconst_1    
         1: iconst_0
         2: idiv
         3: istore_1
         4: iload_1
         5: istore_2
         6: iconst_2
         7: istore_1
         8: iload_2
         9: ireturn
        10: astore_2                          # 从这开始是算术异常开始的地方
        11: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        14: ldc           #8                  // String 我是异常,我被捕捉了
        16: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        19: iconst_m1
        20: istore_1
        21: iconst_2
        22: istore_1
        23: goto          31
        26: astore_3       # 从这个地方开始是异常路径(根据异常表得知,为finally处理的过程)
        27: iconst_2
        28: istore_1
        29: aload_3
        30: athrow
        31: iload_1
        32: ireturn
      Exception table:   # 异常表,表示从from 到 to 之间的代码如果发生type类型的异常,则会执行target指定的代码
         from    to  target type
             0     6    10   Class java/lang/ArithmeticException
             0     6    26   any
            10    21    26   any
      LineNumberTable:
        line 13: 0
        line 14: 4
        line 19: 6
        line 14: 8
        line 15: 10
        line 16: 11
        line 17: 19
        line 19: 21
        line 20: 23
        line 19: 26
        line 20: 29
        line 21: 31
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            4       6     1     i   I
           11      10     2     e   Ljava/lang/ArithmeticException;
           21       5     1     i   I
            0      33     0  this   Lcn/pzhu/impassive/Main;
           29       4     1     i   I
      StackMapTable: number_of_entries = 3
        frame_type = 74 /* same_locals_1_stack_item */
          stack = [ class java/lang/ArithmeticException ]
        frame_type = 79 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
        frame_type = 252 /* append */
          offset_delta = 4
          locals = [ int ]

我们再来一个去掉finally的字节码指令

public int div() {
        int i;
        try {
            i = 1 / 0;
            return i;
        } catch (ArithmeticException e) {
            System.out.println("我是异常,我被捕捉了");
            i = -1;
        }
        return i;
    }
  public int div();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_1
         1: iconst_0
         2: idiv
         3: istore_1
         4: iload_1
         5: ireturn
         6: astore_2
         7: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: ldc           #8                  // String 我是异常,我被捕捉了
        12: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        15: iconst_m1
        16: istore_1
        17: iload_1
        18: ireturn
      Exception table:
         from    to  target type
             0     5     6   Class java/lang/ArithmeticException
      LineNumberTable:
        line 13: 0
        line 14: 4
        line 15: 6
        line 16: 7
        line 17: 15
        line 19: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            4       2     1     i   I
            7      10     2     e   Ljava/lang/ArithmeticException;
            0      19     0  this   Lcn/pzhu/impassive/Main;
           17       2     1     i   I
      StackMapTable: number_of_entries = 1
        frame_type = 70 /* same_locals_1_stack_item */
          stack = [ class java/lang/ArithmeticException ]
}

我们注意看异常表,就少了两行记录。现在我们来分析一下异常抛出的原理。当方法调用发生异常的时候,JVM会查找当前方法的异常表,如果异常表为空,则向上抛出,如果不为空,则根据from、to和type匹配异常类型,如果满足,则行target,如果不满足,则匹配下一条,如果都没有则向上抛出。当添加了finally的时候,type为any,意味着匹配任何异常,有两个是因为一个是在try中的代码的,一个是catch代码的。所以,try中无论是否发生异常,finally都会被执行。(如果try中有死循环或者是调用了System.exit(),则finally不会被执行)

抛出异常

如果当前方法无法处理这个异常,可以选择向上抛出,不处理。

public int div() throws ArithmeticException {
        int i;
        try {
            i = 1 / 0;
            return i;
        } catch (ArithmeticException e) {
            throw new ArithmeticException();
        }
    }

注意,如果抛出的异常是受检异常,上一级需要进行才可以,不然编译不会通过。