Java 异常体系

在 Java 中,离开方法的手段可以 靠 return 方法返回,而异常是 return 方法之外,离开方法的手段

在实际工作中,经常会遇到空指针、找不到、文件找不到,等等不计其数的异常情况。那么 Java 中如何处理这些异常呢?

try / catch / finally

当对一个文件进行写入操作的时候,所使用的 IDE 就会提醒,可能存在异常,需要处理。于是我们按照 IDE 的提示,编写了如下代码

1
2
3
4
5
6
7
8
public static void openFile() {
File file = new File("C:\\Users\\Administrator\\Desktop\\1.txt");
try {
OutputStream os = new FileOutputStream(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}

代码主要分成两部分:try 包裹的部分,以及 catch 包裹部分,其中 catch 专门用来捕获异常。

其中 try 包裹的,表示可能运行会出错的代码。上面的代码通过 File,构建一个输出流,由于 File 对应的文件不一定存在,导致构建输出流失败,出现异常。因此,构建输出流的代码就要放到 try 中包裹起来。

catch 包裹的代码,就是对异常进行处理的流程。上面的代码是将错误信息打印到标准错误流中。

一般异常处理使用 try-catch 语句就足够了,但有遇到加载文件资源的情况,使用完之后就需要及时关闭。也许你会想把关闭资源操作放在 try-catch 语句之后,这也是可以的。但是,一旦 try-catch 中有return 操作,导致代码无法往下执行,关闭资源也就失效了。

finally就是解决这个问题的,无论 try-catch 中如何返回,如何调用,它包裹的代码块都会执行。在 catch 语句后加上 finally,以及包裹的代码,这样就完成了一套标准的异常处理流程。

1
2
3
4
5
6
7
8
9
10
public static void openFile() {
File file = new File("");
try {
OutputStream os = new FileOutputStream(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
//执行资源清理操作
}
}

Tips:如果 try 中可能存在多种类型异常,我们就需要多个 catch 来捕获他们。

try-with-resources

在上面的代码中提到资源清理操作,在生产过程中,可能不止一个资源需要清理,我们需要进行多个资源关闭操作,难免会忘记对些资源清理。因此,在 Java 7 中就引入了 try-with-resources这个语法糖。

原先我们关闭资源操作写在 finally 中,如下

1
2
3
4
5
6
7
8
9
10
11
public static void openFile() throws IOException {
File file = new File("");
OutputStream os = null;
try {
os = new FileOutputStream(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
os.close();
}
}

现在我们把 OutputStream os = new FileOutputStream(file)声明在 try 后面的括号里,声明在里面的东西它会帮我们自动关闭,因此 finally 语句也不用写了。代码如下

1
2
3
4
5
6
7
8
9
10
public static void openFile(){
File file = new File("");
try (OutputStream os = new FileOutputStream(file)) {

} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

那么问题来了,什么类型的资源会自动关闭呢 ?

答案就是所有实现了 AutoCloseable 这个接口的类,都能被自动关闭。比如上面的 FileOutputStream就实现了这个接口。当然智能的 IDE 在我们写 try-catch 的时候都会 给与提示,告诉你可以转成 try-catch-resources模式

抛出异常

上面使用 try-catch 捕获异常,但实际生产环境中也需要抛出异常,不在当前方法处理。手动抛出异常,需要用到关键字 throw

1
2
3
public static void openFile() throws Exception {
throw new Exception();
}

上面代码抛出了一个异常,但没有使用 try-catch 捕获处理。仔细观察的话,你会发现 openFile 方法后面多出了throws Exception,它的意思声明 openFile 方法可能会抛出异常。

那为什么要声明异常呢?直接抛出异常不行吗?

这就源于 Java 的「保护机制」,如果你的方法中可能存在异常,要么使用 try-catch 将它捕获处理,要么就使用 throws 给当前方法声明异常。任何调用了声明异常的方法,都需要处理传过来的异常,与前面一样,要么 try-catch,要么继续声明异常等待其他方法调用处理。

1
2
3
4
5
6
7
public static void A() throws Exception {
throw new Exception();
}

public static void B() throws Exception {
A();
}

如上面代码所示,B 调用 A 方法,A 方法没有 try-catch,而是使用 throws 声明异常。因此,B 方法就有两种选择,要么 try-catch 处理 A 抛出的异常,要么选择像 A 一样,声明一个异常继续向上抛出。显然 B 是同 A 一样,选择声明异常向上抛出,当其他方法调用 B 方法,也会面临 B 方法的两种选择。如果一直向上抛异常,而不使用 try-catch 处理,那么抛出的异常会击穿所有的栈帧,直到有方法将它 catch 住。

Tips:throw 是抛出一个异常,而 throws 则是声明方法可能要抛出的异常,切勿混淆。

Java 异常体系

  • Throwable - 可以被抛出的东⻄(有毒)
    • Exception - checked execption(受检异常,有毒,代表⼀种预料之中的异常)
      • RuntimeException (运⾏时异常,⽆毒,代表⼀种预料之外的异常,因此不需要声明)
    • Error (错误,无毒)
  • catch 的级联与合并

Throwable 是所有 Exception 和 Error 的父类,只要是 Throwable 类型就可以使用 throw 抛出。由于 Error 和 Exception 是他的子类,因此也能被抛出。

上面的继承体系中提到 「有毒」和「无毒」的概念,有毒指的是方法会传染,而无毒不会。「有毒」同前面的 A 方法和 B 方法一样,任何声明了有毒类型的方法,被调用之后,需要再次声明或者自己处理。任何声明了「无毒」类型的方法,被调用之后方法中无需做任何处理。「有毒」的代码实例,参考「抛出异常」章节的 A 和 B 方法。下面展示「无毒」的类型代码

1
2
3
4
5
6
7
8

public static void A(){
throw new RuntimeException();
}

public static void B(){
A();
}

可以看到我们抛出了一个「无毒」类型异常 RuntimeException,当 B 方法调用 A 方法的时候,无需 try-catch,也无需声明异常。

这就是「有毒」和「无毒」的区别,它们有专业的术语 checkedunchecked,表示「受检」和「非受检」。「受检」表示写的代码会被检查,IDE 一般都会提示你做相应的处理。「非受检」表示写的代码不会被检查,无需做处理。

Exception 是预料之内异常,正因为在预料之内,所以 IDE 才会提示我们去处理这个异常。常见的有 IOExceptionFileNotFonudException 。RuntimeException 预料之外的异常,也正因为预料之外,因此 IDE 并不会对抛出的异常进行检验,因为根本无法检验。常见的有 NullPointerException,即空指针异常。

Error 代表一种严重的错误,他与 Exception 的区别是,前者代表不能恢复的异常,后面代表可以恢复的异常。大多数情况下,Error 代表一种不正常的情况,像内存错误(OutOfMemoryError)等。而像网络超时重连就是可恢复异常的表现。

catch 的级联与合并

前面我们提到过,方法中存在多个异常,就需要多个 catch 来处理,如下分别对几种不同类型的异常进行处理,进来的异常会从上到下一次比对,进入到对应的 catch 中

1
2
3
4
5
6
7
8
9
try {

}catch (Exception e){

}catch (EOFException e){

}catch (FileNotFoundException e){

}

但上面代码存在一个问题,Exception 下面的 catch 都不会执行。因为 Exception 是他们的父类,所以每次进来都是第一个 catch 被执行,所以我们应该调整顺序从小到大排列,这样才能根据不同异常类型,做不同处理,如下

1
2
3
4
5
6
7
8
9
try {

}catch (FileNotFoundException e){

}catch (EOFException e){

}catch (Exception e){

}

在 Java 7 之后引用了新的语法,对于处理方法完全一致的异常可以进行合并。如下,都是将错误信息打印到标准错误流中,因此我们可以对它进行合并

1
2
3
4
5
6
try {
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (EOFException e) {
e.printStackTrace();
}

合并之后我们的代码就会变成这样

1
2
3
4
try {
} catch (FileNotFoundException | EOFException e) {
e.printStackTrace();
}

异常的一些原则

抛出原则

  • 能⽤ if / else处理的,不要使⽤异常

    • 无法保证抓出的异常是你想抓住的
    • 相比 if 判断,异常的创建是非常昂贵的操作
  • 尽早抛出异常

  • 异常要准确、带有详细信息

  • 抛出异常也⽐悄悄地执⾏错误的逻辑强的多

处理原则

  • 本⽅法是否有责任处理这个异常?

    • 不要处理不归⾃⼰管的异常
  • 本⽅法是否有能⼒处理这个异常?

    • 如果⾃⼰⽆法处理,就抛出
  • 如⾮万分必要,不要忽略异常

使⽤ JDK 内置的异常

  • NullPointerException
  • ClassNotFoundException/NoClassDefFoundError
  • IllegalStateException
  • IllegalArgumentException
  • IllegalAccessException
  • ClassCastException

Java 异常体系
http://wszzf.top/2021/05/27/Java 异常体系/
作者
Greek
发布于
2021年5月27日
许可协议