Java 异常体系
在 Java 中,离开方法的手段可以 靠 return 方法返回,而异常是 return 方法之外,离开方法的手段。
在实际工作中,经常会遇到空指针、找不到、文件找不到,等等不计其数的异常情况。那么 Java 中如何处理这些异常呢?
try / catch / finally
当对一个文件进行写入操作的时候,所使用的 IDE 就会提醒,可能存在异常,需要处理。于是我们按照 IDE 的提示,编写了如下代码
1 |
|
代码主要分成两部分:try 包裹的部分,以及 catch 包裹部分,其中 catch 专门用来捕获异常。
其中 try 包裹的,表示可能运行会出错的代码。上面的代码通过 File,构建一个输出流,由于 File 对应的文件不一定存在,导致构建输出流失败,出现异常。因此,构建输出流的代码就要放到 try 中包裹起来。
catch 包裹的代码,就是对异常进行处理的流程。上面的代码是将错误信息打印到标准错误流中。
一般异常处理使用 try-catch 语句就足够了,但有遇到加载文件资源的情况,使用完之后就需要及时关闭。也许你会想把关闭资源操作放在 try-catch 语句之后,这也是可以的。但是,一旦 try-catch 中有return 操作,导致代码无法往下执行,关闭资源也就失效了。
finally
就是解决这个问题的,无论 try-catch 中如何返回,如何调用,它包裹的代码块都会执行。在 catch 语句后加上 finally,以及包裹的代码,这样就完成了一套标准的异常处理流程。
1 |
|
Tips:如果 try 中可能存在多种类型异常,我们就需要多个 catch 来捕获他们。
try-with-resources
在上面的代码中提到资源清理操作,在生产过程中,可能不止一个资源需要清理,我们需要进行多个资源关闭操作,难免会忘记对些资源清理。因此,在 Java 7 中就引入了 try-with-resources
这个语法糖。
原先我们关闭资源操作写在 finally 中,如下
1 |
|
现在我们把 OutputStream os = new FileOutputStream(file)
声明在 try 后面的括号里,声明在里面的东西它会帮我们自动关闭,因此 finally
语句也不用写了。代码如下
1 |
|
那么问题来了,什么类型的资源会自动关闭呢 ?
答案就是所有实现了 AutoCloseable
这个接口的类,都能被自动关闭。比如上面的 FileOutputStream
就实现了这个接口。当然智能的 IDE 在我们写 try-catch 的时候都会 给与提示,告诉你可以转成 try-catch-resources模式
抛出异常
上面使用 try-catch 捕获异常,但实际生产环境中也需要抛出异常,不在当前方法处理。手动抛出异常,需要用到关键字 throw
。
1 |
|
上面代码抛出了一个异常,但没有使用 try-catch 捕获处理。仔细观察的话,你会发现 openFile 方法后面多出了throws Exception
,它的意思声明 openFile 方法可能会抛出异常。
那为什么要声明异常呢?直接抛出异常不行吗?
这就源于 Java 的「保护机制」,如果你的方法中可能存在异常,要么使用 try-catch 将它捕获处理,要么就使用 throws 给当前方法声明异常。任何调用了声明异常的方法,都需要处理传过来的异常,与前面一样,要么 try-catch,要么继续声明异常等待其他方法调用处理。
1 |
|
如上面代码所示,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 (错误,无毒)
- Exception - checked execption(受检异常,有毒,代表⼀种预料之中的异常)
- catch 的级联与合并
Throwable 是所有 Exception 和 Error 的父类,只要是 Throwable 类型就可以使用 throw 抛出。由于 Error 和 Exception 是他的子类,因此也能被抛出。
上面的继承体系中提到 「有毒」和「无毒」的概念,有毒指的是方法会传染,而无毒不会。「有毒」同前面的 A 方法和 B 方法一样,任何声明了有毒类型的方法,被调用之后,需要再次声明或者自己处理。任何声明了「无毒」类型的方法,被调用之后方法中无需做任何处理。「有毒」的代码实例,参考「抛出异常」章节的 A 和 B 方法。下面展示「无毒」的类型代码
1 |
|
可以看到我们抛出了一个「无毒」类型异常 RuntimeException
,当 B 方法调用 A 方法的时候,无需 try-catch,也无需声明异常。
这就是「有毒」和「无毒」的区别,它们有专业的术语 checked
和unchecked
,表示「受检」和「非受检」。「受检」表示写的代码会被检查,IDE 一般都会提示你做相应的处理。「非受检」表示写的代码不会被检查,无需做处理。
Exception 是预料之内异常,正因为在预料之内,所以 IDE 才会提示我们去处理这个异常。常见的有 IOException
、FileNotFonudException
。RuntimeException 预料之外的异常,也正因为预料之外,因此 IDE 并不会对抛出的异常进行检验,因为根本无法检验。常见的有 NullPointerException
,即空指针异常。
Error 代表一种严重的错误,他与 Exception 的区别是,前者代表不能恢复的异常,后面代表可以恢复的异常。大多数情况下,Error 代表一种不正常的情况,像内存错误(OutOfMemoryError)等。而像网络超时重连就是可恢复异常的表现。
catch 的级联与合并
前面我们提到过,方法中存在多个异常,就需要多个 catch 来处理,如下分别对几种不同类型的异常进行处理,进来的异常会从上到下一次比对,进入到对应的 catch 中
1 |
|
但上面代码存在一个问题,Exception 下面的 catch 都不会执行。因为 Exception 是他们的父类,所以每次进来都是第一个 catch 被执行,所以我们应该调整顺序从小到大排列,这样才能根据不同异常类型,做不同处理,如下
1 |
|
在 Java 7 之后引用了新的语法,对于处理方法完全一致的异常可以进行合并。如下,都是将错误信息打印到标准错误流中,因此我们可以对它进行合并
1 |
|
合并之后我们的代码就会变成这样
1 |
|
异常的一些原则
抛出原则
能⽤ if / else处理的,不要使⽤异常
- 无法保证抓出的异常是你想抓住的
- 相比 if 判断,异常的创建是非常昂贵的操作
尽早抛出异常
异常要准确、带有详细信息
抛出异常也⽐悄悄地执⾏错误的逻辑强的多
处理原则
本⽅法是否有责任处理这个异常?
- 不要处理不归⾃⼰管的异常
本⽅法是否有能⼒处理这个异常?
- 如果⾃⼰⽆法处理,就抛出
如⾮万分必要,不要忽略异常
使⽤ JDK 内置的异常
- NullPointerException
- ClassNotFoundException/NoClassDefFoundError
- IllegalStateException
- IllegalArgumentException
- IllegalAccessException
- ClassCastException