聊聊 Java Exception 机制

调用 Retrofit 同步方法常会写成以下代码的形式,catch 语句容易漏掉,而编译器也不提醒你。

1
2
3
4
5
6
7
8
9
new Thread() {
public void run() {
try {
mRestAdapter.create(Api.class).sendRequst();
} catch (Exception e) {
// handle Exception
}
}
}.start();

但当你使用 BufferedReader 时,编译器死命地提醒你,这有一个 IOException,非让你处理了才行。

1
2
3
4
5
6
try {
BufferedReader reader = new BufferedReader(new FileReader(Path));
String firstLine = reader.readLine();
} catch (IOException e) {

}

那么问题来了,这两种 Exception 有什么区别使得编译器区别对待呢?

Exception 机制

这就得从 Java 的 Exception 机制开始说起了。

Throwable 表示任何可以作为异常被抛出的类。Throwable 对象分为两种类型,Error 表示系统级错误,如资源不足、约束失败,或者其他使程序无法继续执行的条件,一般无需我们关心。而 Exception 是所有异常的父类,可分为两大类型。一种是 RuntimeException 及其子类,表示运行时异常。另一种是除了 RuntimeException 及其子类之外的 Exception,称为受检异常。
那运行时异常和受检异常有什么区别呢?在使用上最大的区别就是,受检异常强迫调用者使用显式的 catch 语句处理或者将它传播出去。写代码时如果 IDE 提示你使用 catch 语句或者在方法中添加 throws 声明,这种异常都是受检异常,要扼杀在摇篮里,否则编译都无法通过。而运行时异常则不需要这样做。
除了使用上的差异,两者的使用场景也有区别。《Effective Java》是这样建议的:如果可恢复的情况使用受检异常,对编程错误使用运行时异常。

让我们回到问题,现在答案已经很明显了。Retrofit 抛出的是运行时异常,而 BufferedReader 抛出的是受检异常。

那 Retrofit 为什么不抛出受检异常呢?这种情况应该属于可恢复的,并且受检异常能提醒开发者进行处理。
不是不想,实是不能啊。Retrofit 是通过动态代理来实现声明的接口的功能。而由于父类方法没有抛出异常,子类方法也不能显式地抛出异常。所以 Retrofit 不能抛出各种异常(如 IO 异常),只能通过捕获异常并转换为 RuntimeException 再抛出。

参考

  • Effective Java 第9章 异常