Java引入了已检异常的概念。与早期的方法相比,强制开发人员管理异常的想法是革命性的。
现在,Java仍然是唯一广泛使用的提供已检异常的语言。例如,Kotlin中的每个异常都是未检查的。
即使在Java中,新特性也和已检异常不一致:Java内置函数式接口的签名不使用异常。在lambda中集成遗留代码时,代码会变得很麻烦。这在溪流中很明显。
在这篇文章中,我想深入探讨如何管理这样的问题。
下面的示例代码说明了这个问题:
不能编译:需要捕获已检查的ClassNotFoundException我们必须添加一个try/catch块来修复编译问题。
Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList")
.map(it -> new ForNamer().apply(it)) // 1
.forEach(System.out::println);
添加代码块违背了管道易于阅读的目的。
为了恢复可读性,我们需要重构代码以引入一个新类。IntelliJ IDEA甚至提出了一个记录:
var forNamer = new ForNamer(); // 1
Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList")
.map(forNamer::apply) // 2
.forEach(System.out::println);
record ForNamer() implements Function> {
@Override
public Class> apply(String string) {
try {
return Class.forName(string);
} catch (ClassNotFoundException e) {
return null;
}
}
}
项目Lombok是一个编译时注释处理器,它会生成额外的字节码。使用正确的注释就可以得到结果,而无需编写样板代码。
Lombok提供了@SneakyThrow注释:它允许抛出已检异常,而无需在方法签名中声明它们。然而,目前它并不适用于现有的API。
如果你是Lombok用户,请注意,状态停放有一个打开的GitHub问题。
Apache Commons Lang是一个古老的项目。它在当时很流行,因为它提供了一些本可以成为Java API一部分的实用程序,但却没有。这比在每个项目中重新发明你的DateUtils和StringUtils要好得多。在研究这篇文章时,我发现它仍然使用很棒的api定期维护。其中之一是可失败的API。
该API由两部分组成:
Stream
这是一小段摘录:
代码最终变成了我们从一开始就期望的样子:
Stream stream = Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList");
Failable.stream(stream)
.map(Class::forName) // 1
.forEach(System.out::println);
上述代码在运行时抛出ClassNotFoundException异常,并将其封装在UndeclaredThrowableException中。我们满足了编译器的要求,但是我们没有办法指定预期的行为:
为了实现这一点,我们可以利用Vavr的力量。Vavr是一个将函数式编程的强大功能引入Java语言的库:
假设我们需要一个管道来收集异常和类。下面是该API的一个片段,描述了几个构建块:
它翻译成以下代码:
Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList")
.map(CheckedFunction1.liftTry(Class::forName)) // 1
.map(Try::toEither) // 2
.forEach(e -> {
if (e.isLeft()) { // 3
System.out.println("not found:" + e.getLeft().getMessage());
} else {
System.out.println("class:" + e.get().getName());
}
});
到目前为止,我们都在Java流的世界里。它按预期工作,直到forEach,这看起来并不“漂亮”。
Vavr确实提供了自己的Stream类,它模仿了Java的Stream API并添加了额外的功能。让我们用它来重写管道:
var result = Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList")
.map(CheckedFunction1.liftTry(Class::forName))
.map(Try::toEither)
.partition(Either::isLeft) // 1
.map1(left -> left.map(Either::getLeft)) // 2
.map2(right -> right.map(Either::get)); // 3
result._1().forEach(it -> System.out.println("not found: " + it.getMessage())); // 4
result._2().forEach(it -> System.out.println("class: " + it.getName())); // 4
Java最初的设计大量使用了已检异常。编程语言的发展证明了这不是一个好主意。
Java流不能很好地处理已检异常。将后者集成到前者所需的代码看起来并不好。为了恢复流的可读性,我们可以使用Apache Commons Lang。
编译只是问题的一小部分。我们通常希望对异常采取行动,而不是停止管道或忽略异常。在这种情况下,我们可以利用Vavr库,它提供了一种更函数式的方法。