Java Checked Exception 的是与非

发布时间 2023-04-02 21:53:57作者: 忘路之远近

结论

Java Checked Exception是一个设计错误,初衷很美好,现实很糟糕。

设计的初衷

把方法可能抛出的异常,显示地声明在方法定义中,比如FileInputStream的构造函数可能会抛出FileNotFoundException:

public FileInputStream(String name) throws FileNotFoundException {
    this(name != null ? new File(name) : null);
}

从而

  1. 明确列出有哪些可能出现的异常需要被处理,表意更清晰,可读性更好
  2. 编译器也会强制调用方做异常处理(声明的异常不是RuntimeException)

糟糕的现实

然而美好的设计初衷落到现实的代码里,往往可能变成下面这样:

  1. 一路向上的异常声明
public void readFile() throws FileNotFoundException {
    FileInputStream fileInputStream = new FileInputStream("a.txt");
    // ...
}

调用链路长,异常多时,是一种灾难。

  1. 抛出被RuntimeException包装的异常
public void readFile() {
    try {
        FileInputStream fileInputStream = new FileInputStream("a.txt");
        // ...
    } catch (FileNotFoundException e) {
        throw new RuntimeException(e);
    }
}

这种实现更常见,这样以来Checked Exception形同虚设。

  1. 版本升级,方法实现有变动,声明的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时,可以

  1. 用上文所述抛出RuntimeException包装的异常,Throwable#getCause() 获取原异常:
try {
    new Demo().readFile();
} catch (Exception exception) {
    Throwable cause = exception.getCause();
    if (cause instanceof FileNotFoundException) {
        // instanceOf 判断原异常类型分别处理
        // ...
    }
}
  1. 不关心原异常具体类型时,使用lombok的@SneakyThrows
@lombok.SneakyThrows
public void readFile() {
    FileInputStream fileInputStream = new FileInputStream("a.txt");
    // ...
}

一些参考