Java-异常体系

Java-异常体系

sschrodinger

2019/03/08


基于 JAVA API 11

参考 关于 Java 中 finally 语句块的深度辨析

参考 JLS 标准

参考 JVM 标准


异常体系的分类


在 Java 异常体系中,所有的异常都继承自 Throwable 类,具体又分为异常错误两种异常。

错误是继承了 Throwable 类的 Error 类及其子类的所有类,比较典型的是 OutOfMemoryErrorStackOverflowError 两种错误。所有的错误都会被 Java 虚拟机捕获,表示的是不可挽回的错误,程序员没有方法对这些错误进行处理。

异常是继承了 Throwable 类的 Exception 类及其子类的所有类,又分为检查型异常和非检查型异常,检查型异常(Checked Exception)指的是需要程序员对此异常进行处理,包括捕捉或抛出错误,非检查型异常(Unchecked Exception)指的是不需要程序员对此异常进行处理。

JVM 和 Java 编译器会对异常进行检查,所有RuntimeException 类都是属于非检查型异常,包括RuntimeExceptu+ionNullPointerException 等,其他的都属于检查型异常。


异常的使用


对于检查型异常,必须对其进行捕获或抛出到下一级函数,抛出用关键词 throws 申明,捕获使用 try{}catch{}finally{} 语句块申明。

代码如下:

public class Demo() {
    
    public function_1() {
        try {
            //使用 try catch 捕获错误
            function_2();
        } catch(IOException e) {
            
        } finally {
            //可选项
        }
    }
    //如果不捕获,则函数申明必须抛出错误
    public function_2() throws IOException {
        function_3();
    }
    
    //如果不捕获,则函数申明必须抛出错误
    public finction_3() throws IOException {
    
        //抛出一个非检查异常
        throw new IOException();
    }
    
}

对于检查型异常,如果没有抛出或者捕获错误,编译器会报错,但是对于非检查型错误,编译器不强制要求进行捕捉或者抛出,如下:

public class Test {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Test tset = new Test();
        tset.testFunc();
    }
    
    public void testFunc() {
        //也可进行捕获
        try {
            testFunc1();
        } catch (DemoUnCheckedException e) {
            // TODO: handle exception
            System.out.println(e.getClass().getName());
        }
    }
    
    //在此处可以不进行捕获或者抛出
    public void testFunc1() {
        testFunc2();
    }
    
    public void testFunc2() {
        throw new DemoUnCheckedException();
    }
}

class DemoUnCheckedException extends RuntimeException {
    
}

try-catch-finally 语句块


问题一:当try中有return语句时,finally语句是否会运行?

答案是会。

oracle的官方文档解答了这个问题。

the finally block always executes when the try block exits. This ensures that the finally block is executed even if an unexpected exception occurs. But finally is useful for more than just exception handing - it allows the programmer to avoid having cleanup code accidentally bypassed by a return, continue, or break. Putting cleanup code in finally blockis always a good practice, even when no exception are anticipated.

Note

  • If the JVM exits while the try or catch code is being executed, then the finally block may not execute. Likewise, If the thread executing the try or catch code is interruped or killed, the finally block may not execute even though the application as a whole continues.

原文翻译如下:当try语句退出时肯定会执行finally语句。这确保了即使是发生了一个意想不到的异常也会执行finally语句块。但是finally的用处不仅是用来处理异常-它可以让程序员不会因为return、continue或者break语句而忽略了清理代码的工作。把清理代码放在finally语句块中是一个很好的做法,即使可能不会有异常发生也要这么做。

问题二:finally语句块执行的时机?

try-catch 语句块中,没有流程跳转指令时,如没有 returnbreakcontinue时,finally语句块的执行会紧跟在 try-catch 之后,但是当 try-catch 中有流程跳转指令时,finally 语句块的执行顺序就变得很复杂。

首先来看一个例子:

public class Test { 
    public static void main(String[] args) {  
        testFunc();
        System.out.println("last function");
    } 
    
    public static void testFunc() {
        try {  
            System.out.println("try block");  
            return;
        } finally {  
            System.out.println("finally block");  
        }  
    }
}
//output:
//try block
//finally block
//last function

可以看到,finally 语句块在返回之前执行。

更准确的说,finally 语句都是在控制转移语句之前执行。Java 语言规范如下:

JLS

  • Afinallyclause can also be used to clean up forbreak,continue, andreturn, which is one reason you will sometimes see atryclause with nocatchclauses. When any control transfer statement is executed, all relevantfinallyclauses are executed. There is no way to leave atryblock without executing itsfinallyclause.

问题三:当 finally 语句块和 try-catch 语句块都有返回值时,返回值是什么?

返回值为 finally 语句块中的值。

我们看如下代码:

public class Test { 
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++; 
       } 
    } 
}
//output:
//return value of getValue(): 1

原理比较简单,当执行到 return i 时,会将返回值缓存在变量空间中,返回时直接返回缓存的值。

这在 JVM 规范中也有规定,规定原文如下:

JVM Specification

  • If the try clause executes a return, the compiled code dose the folloing:
    • Saves teh return value(if any) in a local variable.
    • Executes a jsr to the code for the finally clause
    • Upon return from the finally clause, returns the value saves in the local variable.

即在第一次执行return语句时,就保存了返回值在本地变量中。

这主要是 Java 编译时实现的功能,看上面示例 class 文件如下:

public class Test extends java.lang.Object{ 
public Test(); 
 Code: 
  0:    aload_0 
  1:invokespecial#1; //Method java/lang/Object."":()V 
  4:    return 
 
 LineNumberTable: 
  line 1: 0 
 
public static void main(java.lang.String[]); 
 Code: 
  0:    getstatic   #2; //Field java/lang/System.out:Ljava/io/PrintStream; 
  3:    new     #3; //class java/lang/StringBuilder 
  6:    dup 
  7:    invokespecial   #4; //Method java/lang/StringBuilder."":()V 
  10:   ldc     #5; //String return value of getValue(): 
  12:   invokevirtual   
  #6; //Method java/lang/StringBuilder.append:(
      Ljava/lang/String;)Ljava/lang/StringBuilder; 
  15:   invokestatic    #7; //Method getValue:()I 
  18:   invokevirtual   
  #8; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 
  21:   invokevirtual   
  #9; //Method java/lang/StringBuilder.toString:()Ljava/lang/String; 
  24:   invokevirtual   #10; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 
  27:   return 
 
public static int getValue(); 
 Code: 
  0:    iconst_1 
  1:    istore_0 
  2:    iload_0 
  3:    istore_1 
  4:    iinc    0, 1 
  7:    iload_1 
  8:    ireturn 
  9:    astore_2 
  10:   iinc    0, 1 
  13:   aload_2 
  14:   athrow 
 Exception table: 
  from   to  target type 
    2     4     9   any 
    9    10     9   any 
}

我们看异常处理表,from to target 的含义时,如果在 formto 之间出现了异常,则从 taget 开始运行。

我们先看正常运行过程,正常程序在 code line 3 时,将 1 加入 正常本地变量缓存表,然后执行 i++ ,即 第 4 行语句,第 5 行语句取出缓存表然后返回。

不正常的运行流程如下,当从 2 到 4 这段指令出现异常时,将会产生一个 exception 对象,并且把它压入当前操作数栈的栈顶。接下来是 astore_2 这条指令,它负责把 exception 对象保存到本地变量表中 2 的位置,然后执行 finally 语句块,待 finally 语句块执行完毕后,再由 aload_2 这条指令把预先存储的 exception 对象恢复到操作数栈中,最后由 athrow 指令将其返回给该方法的调用者(main)

你可能感兴趣的:(Java-异常体系)