关于 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。这样:
- 调用的代码每次都需要检查null值。
- 没有办法知道文件是不存在还是不可读。
使用更函数化的实现来修复第一个问题,这样允许方法组合为:
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
- 等等
这样,你可以让异常冒泡到它们应该被处理的地方。