Java中的异常处理

写 bug 的大耳朵图图
·

1. Java 中的异常分类

Java中的异常类均以Throwable为父类,而Throwable又派生出 ErrorException 两类,区别如下

1.1 Error类及其子类

代表了JVM自身的异常。这一类异常发生时,无法通过程序来修正。例如系统崩溃、内存溢出等。与异常不同,错误表示程序无法继续执行下去,一般不需要进行捕获或处理。错误通常是由底层系统或环境导致的,它们是不可控的,最可靠的方式就是尽快地停止JVM的运行。

1.2 Exception类及其子类

Exception又分为运行时异常(RuntimeException)和非运行时异常, 这两种异常有很大的区别,也称之为非受检异常(Unchecked Exception)和受检异常(Checked Exception),其中Error类及其子类也是非受检异常。

  • 受检异常:也称为“编译时异常”,编译器在编译期间检查的那些异常。由于编译器“检查”这些异常以确保它们得到处理,因此称为“检查异常”。如果抛出检查异常,那么编译器会报错,需要开发人员手动处理该异常,要么捕获,要么重新抛出。除了RuntimeException之外,所有直接继承 Exception 的异常都是检查异常。

  • 非受检异常:也称为“运行时异常”,编译器不会检查运行时异常,在抛出运行时异常时编译器不会报错,当运行程序的时候才可能抛出该异常。Error及其子类和RuntimeException 及其子类都是非检查异常。

Java 中异常类的关系可以使用如下 UML 类图表示

iShot_2023-08-12_22.18.46.jpg

受检异常和非受检异常是针对编译器而言的,是编译器来检查该异常是否强制开发人员处理该异常:

  • 受检异常导致异常在方法调用链上显式传递,而且一旦底层接口的检查异常声明发生变化,会导致整个调用链代码更改。

  • 使用非受检异常不会影响方法签名,而且调用方可以自由决定何时何地捕获和处理异常。

1.3 受检异常举例

image-20230813204926255.png

编译器提示需要处理这个异常,这种异常处理有两种方式:

  • 在方法签名上抛出此异常,由方法调用方处理
  • 使用try-catch 捕获异常,内部处理

image-20230813205125690.png

1.4 非受检异常异常举例

所有继承 RuntimeException 的异常都是非检查异常,直接抛出非检查异常编译器不会提示错误

image-20230813205245428.png

方法直接抛出 RuntimeException 时,编译器并不会要求捕获或者抛出此异常。

2. try-catch

try-catch 关键字在Java 中主要用于捕获异常,并进行处理。简单示例如下:

image-20230813205659417.png

在 try{} 代码块中,是可能抛出异常的代码或者调用了签名上会抛出异常的方法。cath{} 代码块中则是捕获异常,并处理异常。注意:catch 可以捕获多种异常,并根据异常种类不同,分开处理,但是要注意异常捕获的顺序。

image-20230813210343797.png

在上面的示例中,先捕获了 IOException,IDE 就会提示下面的 FileNotFoundException 无需再被捕获了,因为 IOException 是 FileNotFoundException 的父类,捕获到 IOException 之后,其所有子类的异常捕获代码都会失效。

下面演示如何同时捕获多个异常,并用同一个分支处理:

image-20230813210712437.png

当我们需要对多个异常分组处理时,可以使用 catch(Exception1 | Exception2 e) 来捕获多个异常。

3. try-catch-finally

try-catch-finally 用于在处理异常时,不管是否发生异常,都要执行的操作。示例代码如下:

try 代码块中发生了异常:

image-20230813211231664.png

提问:为什么先打印了 finally 代码块中的内容,后打印了异常信息?

try 代码块未异常:

image-20230813211411419.png

finally{} 一般用于资源的关闭,或者数据的清理, 但是也可以在 finally 中执行 return 命令来修改方法返回。示例代码如下:

image-20230813211910857.png

提问:大家觉得这个cal 方法返回值是多少?为什么?

正常情况下,finally 代码块中的代码一定是会执行的,但是也有以下几种失效情况:

  1. 在执行 try 或 catch 块之前 JVM 被非法终止,比如程序正在运行,但是使用 pkill -9 java 命令强行停止 Java 进程。

  2. 在 try 或 catch 块中发生了 System.exit() 调用,导致 JVM 直接退出。

image-20230813212533105.png

  1. 在 try 或 catch 块中发生了死循环,导致程序无法继续执行。

  2. 在 try 或 catch 块中发生了栈溢出异常(StackOverflowError)或虚拟机异常(如 OutOfMemoryError),导致 JVM 崩溃。

  3. 程序所在的线程被强制中断或程序进程被操作系统杀死。

  4. 在 try 或 catch 块中使用了 System.halt() 方法,显式终止 JVM。

  5. 调用了 native 方法,而该方法中不包含 finally 块。

4. try-with-resources 用法

try-with-resources 是 Java 7 引入的一个语法结构,用于更加方便地处理需要关闭的资源。它可以自动关闭实现了 AutoCloseableCloseable 接口的资源,无需手动编写 finally 块来关闭资源。try-with-resources 的语法形式是在 try 关键字之后使用圆括号括起来的资源声明列表。每个资源在括号中声明并初始化。当 try 块结束时,无论是否发生异常,这些资源都将自动关闭,而不需要显式调用 close() 方法。以下是一个读取文件并自动关闭流的示例:

image-20230813213416710.png

FileInputStream 之所以可以自动关闭,是因为其继承了 InputStream 类,而InputStream类实现了 Closeable 接口,FileInputStream重写了 close()方法,以下是具体实现:

image-20230813213712104.png

那如何证明使用 try-with-resources 时,close 方法真的被调用了呢?我们可以使用如下命令编译 App.java 文件,并看下生成的字节码文件

# -g 参数用于生成与调试相关的信息,包括调试符号和源代码行号。它允许在编译后的字节码中插入调试信息,以便在调试过程中可以精确地映射回源代码的行号和变量名
javac -g App.java

生成的 class 文件如下: image-20230813214358657.png

从上面的 class 文件中我们可以清楚看到 jvm 帮我们生成了一个 catch 代码块,用来捕获外层 try 代码块可能抛出的异常,并且在 catch 代码块中显式调用了 fis 的 close() 方法进行资源关闭。这就是为什么说 无论是否发生异常,这些资源都将自动关闭

5. 异常处理规范

异常处理规范参考 《阿里巴巴代码开发规范》 中的约束。

image-20230813215452165.png

image-20230813215516364.png

3
社区准则 博客 联系 社区 状态
主题