发布于 4年前

关于 Java 检查异常 (checked exception)和非检查异常(unchecked exception)

在这篇博文里,我们思考下检查异常 (checked exception)和非检查异常(unchecked exception),特别是它们在函数式编程里的影响。
十几年前Java出现时,在当时它是相当有创意的。特别是它的异常处理机制,相对先前的C/C++有了很大的提高。例如,读取文件可以出现很多异常:文件可以不存在,可以为只读等等。

相关Java的伪代码类似于:

File file = new File("/path");
if (!file.exists) {
    System.out.println("文件不存在");
} else if (!file.canRead()) {
    System.out.println("文件不可读");
} else {
    // 最后读取文件,此处省略相关代码
}

分离try catch代码块的想法背后是分离业务代码和异常处理代码。

try {
    File file = new File("/path");
    // 读取文件,此处省略相关代码
} catch (FileNotFoundException e) {
    System.out.println("文件不存在);
} catch (FileNotReadableException e) {
    System.out.println("文件不可读");
}

当然,上面单独的代码是没用的。它可能构成读文件的专有方法。

public String readFile(String path) {
    if (!file.exists) {
        return null;
    } else if (!file.canRead()) {
        return null;
    } else {
        // 读取文件,此处省略相关代码
        return content;
    }
}

上面catch块的代码有一个问题:它返回的是null。这样:

  1. 调用的代码每次都需要检查null值。
  2. 没有办法知道文件是不存在还是不可读。

使用更函数化的实现来修复第一个问题,这样允许方法组合为:

public Optional<String> readFile(String path) {
    if (!file.exists) {
        return Optional.empty();
    } else if (!file.canRead()) {
        return Optional.empty();
    } else {
        // Finally read the file
        // Depending on the language
        // This could span seveal lines
        return Optional.of(content);
    }
}

可惜,这一点都没有改变第二个问题。那些追求纯粹函数式的实现的人可能要废弃之前的代码片段,而改为:

public Either<String, Failure> readFile(String path) {
    if (!file.exists) {
        return Either.right(new FileNotFoundFailure(path));
    } else if (!file.canRead()) {
        return Either.right(new FileNotReadableFailure(path));
    } else {
        // Finally read the file
        // Depending on the language
        // This could span seveal lines
        return Either.left(content);
    }
}

这相对于先前的代码有了很好的改进。现在的代码更加有意义,因为如果失败,右边的返回值会告诉失败的原因。

但还有一个问题,而且不是小问题。调用代码该怎么写呢?它需要处理失败。或者更加可能的做法是,让调用代码处理,依此类推,知道最上面的代码。

例如,在Go是这样的:

items, err := todo.ReadItems(file) if err != nil { fmt.Errorf("%v", err) }

如果代码在这里结束就相当好了。然而,err必须传给调用的代码,如上所述,是一直往上传。当然,有一个关键词panic,但它似乎不是处理异常的首选方式。

最奇怪的部分,这也正是人们对Java检查异常的抱怨的地方:它需要在异常出现的地方处理,方法的签名也需要做出相应的改变。

因此,我很喜欢非检查异常。唯一的缺点是它打破了纯粹的函数式编程——异常抛出被认为是副作用。除非你使用纯粹的函数式编程,否则没有必要避免使用非检查异常。

此外,编程语言和框架可以在最顶层提供处理异常的钩子,如在JVM,他们包括:

  • JDK,Thread.setDefaultUncaughtExceptionHandler()
  • Vaadin,VaadinSession.setErrorHandler()
  • Spring MVC,@ExceptionHandler
  • 等等

这样,你可以让异常冒泡到它们应该被处理的地方。

©2020 edoou.com   京ICP备16001874号-3