CH06-异常机制

概览

Java 异常是 Java 提供的一种识别及响应错误的机制,Java 异常机制可以使程序中异常处理的代码和正常业务代码分离,保证程序代码更加优雅,并提高程序的健壮性。

层次结构

异常是指非预期的各种状况,如:文件找不到、网络连接失败、非法参数等。异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程。Java 通过 API 中 Throwable 类的众多子类描述各种不同的异常。因此,Java 异常都是对象,是 Throwable 子类的实例,描述了出现在一段编码中的错误条件。当条件生成时,错误将引发异常。

NAME

Throwable

Throwable 是 Java 语言中所有错误与异常的超类。

Throwable 包含两个子类:Error(错误)和 Exception(异常),它们通常用于指示发生了异常情况。

Throwable 包含了其线程创建时线程执行堆栈的快照,它提供了 printStackTrace() 等接口用于获取堆栈跟踪数据等信息。

Error

Error 类及其子类:程序中无法处理的错误,表示运行应用程序中出现了严重的错误。

此类错误一般表示代码运行时 JVM 出现问题。通常有 Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。比如 OutOfMemoryError:内存不足错误;StackOverflowError:栈溢出错误。此类错误发生时,JVM 将终止线程。

这些错误是不受检异常,非代码性错误。因此,当此类错误发生时,应用程序不应该去处理此类错误。按照 Java 惯例,我们是不应该实现任何新的 Error 子类的!

Exception

程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。

  • 运行时异常
    • 都是 RuntimeException 类及其子类异常,如 NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
    • 运行时异常的特点是 Java 编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用 try-catch 语句捕获它,也没有用 throws 子句声明抛出它,也会编译通过。
  • 编译期异常
    • 是 RuntimeException 以外的异常,类型上都属于 Exception 类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如 IOException、SQLException 等以及用户自定义的 Exception 异常,一般情况下不自定义检查异常。

受检异常/非受检异常

  • 受检异常:编译器妖气必须处理的异常
    • 正确的程序在运行中,很容易出现的、情理可容的异常状况。可查异常虽然是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这种异常状况,就必须采取某种方式进行处理。
    • 除了 RuntimeException 及其子类以外,其他的 Exception 类及其子类都属于受检异常。这种异常的特点是 Java 编译器会检查它,也就是说,当程序中可能出现这类异常,要么用 try-catch 语句捕获它,要么用 throws 子句声明抛出它,否则编译不会通过。
  • 非受检异常:编译器不强制要求检查的异常
    • 包括运行时异常(RuntimeException与其子类)和错误(Error)。

异常基础

关键字

  • try:监听异常
  • catch:捕获异常
  • finally:总是执行
  • throw:抛出异常
  • throws:声明异常

异常捕获

  • try-catch
  • try-catch-finally
  • try-finally
  • try-with-resource: AutoCloseable

常见异常

  • RuntimeException
    • ArrayIndexOutOfBoundsException:数组索引越界
    • ArithmeticException:除零异常
    • NullPointerException:空指针
    • ClassNotFoundException:类加载
    • NegativeArraySizeException:数组长度为负
    • ArrayStoreException:数组元素类型不兼容
    • SecurityException:安全性异常
    • IllegalArgumentException:非法参数
  • IOException
    • IOException:输出输出流异常
    • EOFException:文件结束
    • FileNotFoundException:文件未找到
  • Others
    • ClassCastException
    • SQLException
    • NoSuchFieldException
    • NoSuchMethodException
    • NumberFormatException
    • StringIndexOutOfBoundsException
    • IllegalAccessException
    • InstantiationException

应用实践

  • 在恰当的级别处理异常,即知道该如何处理的地方及时处理异常。
  • 解决问题并且重新调用产生异常的方法。
  • 进行少许修补,然后绕过异常发生的地方继续执行。
  • 用别的数据进行计算,以代替方法预计会返回的值。
  • 把当前运行环境下能做的事尽量做完,然后把相同的异常重抛到更高层。
  • 终止程序。
  • 进行简化。
  • 让类库和程序更安全。
  • 不要通过异常来实现业务流程。

底层机制

try-catch

public static void simpleTryCatch() {
   try {
       testNPE();
   } catch (Exception e) {
       e.printStackTrace();
   }
}

通过 javap 的带编译后的代码:

//javap -c Main
 public static void simpleTryCatch();
    Code:
       0: invokestatic  #3                  // Method testNPE:()V
       3: goto          11
       6: astore_0
       7: aload_0
       8: invokevirtual #5                  // Method java/lang/Exception.printStackTrace:()V
      11: return
    Exception table:
       from    to  target type
           0     3     6   Class java/lang/Exception

异常表中包含了一个或多个异常处理器的信息,内容如下:

  • from:可能发生异常的起始点
  • to:可能发生异常的结束点
  • target:从 from 到 to 之间发生异常后异常处理器的位置
  • type:异常处理器所处理的异常类型

当发生一个异常时:

  1. JVM 会在当前出现异常的方法中查找异常表,检查是否有合适的处理器
  2. 如果当前方法的异常表不为空,且异常符合表中 from to 的范围,同时 type 匹配,则 JVM 将调用位于 target 的处理器来处理异常
  3. 如果上一步没有找到处理器,则继续检查异常表中的下一项
  4. 如果当前异常表无法处理,则向上查找(弹栈)调用方法的调用点,并重复 1-3 步的操作。
  5. 如果所有栈帧都被弹出,仍然没有处理,则抛出异常给当前 Thread,Thread 会终止。
  6. 如果当前 Thread 为最后一个非守护线程,且异常未处理,则导致 JVM 终止运行。

try-catch-finally

public static void simpleTryCatchFinally() {
   try {
       testNPE();
   } catch (Exception e) {
       e.printStackTrace();
   } finally {
       System.out.println("Finally");
   }
}

通过 javap 得到异常表部分:

public static void simpleTryCatchFinally();
    Code:
       0: invokestatic  #3                  // Method testNPE:()V
       3: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
       6: ldc           #7                  // String Finally
       8: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      11: goto          41
      14: astore_0
      15: aload_0
      16: invokevirtual #5                  // Method java/lang/Exception.printStackTrace:()V
      19: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
      22: ldc           #7                  // String Finally
      24: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      27: goto          41
      30: astore_1
      31: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
      34: ldc           #7                  // String Finally
      36: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      39: aload_1
      40: athrow
      41: return
    Exception table:
       from    to  target type
           0     3    14   Class java/lang/Exception
           0     3    30   any
          14    19    30   any
  • 如果 0-3 之间发生了 Exception 异常,调用 14 位置的处理器。
  • 如果 0-3 之间无论发生哪种异常,都调用 30 位置的处理器。
  • 如果 14-19(catch) 之间无论发生什么异常,都调用 30 位置的处理器。

注意异常表会代码中 finally 的部分同时在异常表的 catch 和 finally 部分注册,以实现无论是否发生异常都执行的逻辑。

catch 先后顺序

先 catch 类层级较高的异常类型会导致编译错误。

return 与 finally

public static String tryCatchReturn() {
   try {
       testNPE();
       return  "OK";
   } catch (Exception e) {
       return "ERROR";
   } finally {
       System.out.println("tryCatchReturn");
   }
}

这里无论是否抛出一查,finally 部分都会执行。

性能损耗

创建一个异常对象是创建一个普通对象所需耗时的几十倍,抛出、捕获异常则是创建异常对象所需耗时的数倍。