结论
Java Checked Exception是一个设计错误,初衷很美好,现实很糟糕。
设计的初衷
把方法可能抛出的异常,显示地声明在方法定义中,比如FileInputStream的构造函数可能会抛出FileNotFoundException:
public FileInputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null);
}
从而
- 明确列出有哪些可能出现的异常需要被处理,表意更清晰,可读性更好
- 编译器也会强制调用方做异常处理(声明的异常不是RuntimeException)
糟糕的现实
然而美好的设计初衷落到现实的代码里,往往可能变成下面这样:
- 一路向上的异常声明
public void readFile() throws FileNotFoundException {
FileInputStream fileInputStream = new FileInputStream("a.txt");
// ...
}
调用链路长,异常多时,是一种灾难。
- 抛出被RuntimeException包装的异常
public void readFile() {
try {
FileInputStream fileInputStream = new FileInputStream("a.txt");
// ...
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
这种实现更常见,这样以来Checked Exception形同虚设。
- 版本升级,方法实现有变动,声明的Checked Exception也需要改变时,需要新的方法定义
假如FileInputStream构造器改变了实现,需要在声明上增加AccessDeniedException:
public FileInputStream(String name) throws FileNotFoundException, AccessDeniedException {
this(name != null ? new File(name) : null);
// ... 实现上的变动
}
直接像上面这样改方法声明是无法实现的,因为无数的上游调用方只处理了FileNotFoundException,会有兼容问题,只能通过其他方式。升版困难,扩展性差。
为什么有设计与实现的背离
对于上述现实里的现象1和现象2,产生这种背离的一般原因都是:方法的调用方没有异常处理的能力。
文件读取、网络调用这类服务的调用方可能处于应用结构的下层,而对不同的异常做出不同的处理决策往往是较上层组件的能力(展示给用户友好的异常提示、自动容灾策略等)。
对于现实里的现象3,方法升级时,仅仅为了增加异常声明就大费周章升级接口,实在不划算;不过不添加异常声明,改用RuntimeException却又显得不统一。左也不是,右也不是。
我该怎么做
一言以蔽之,日常开发中,避免使用Checked Exception。
碰到库中Checked Exception时,可以
- 用上文所述抛出RuntimeException包装的异常,Throwable#getCause() 获取原异常:
try {
new Demo().readFile();
} catch (Exception exception) {
Throwable cause = exception.getCause();
if (cause instanceof FileNotFoundException) {
// instanceOf 判断原异常类型分别处理
// ...
}
}
- 不关心原异常具体类型时,使用lombok的@SneakyThrows
@lombok.SneakyThrows
public void readFile() {
FileInputStream fileInputStream = new FileInputStream("a.txt");
// ...
}
一些参考
- Kotlin放弃了Checked Exception,给出了一些理由,引用了下面两篇想法
- Java's checked exceptions were a mistake
- The Trouble with Checked Exceptions