Lambda表达式中的异常

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);

添加代码块违背了管道易于阅读的目的。

将Try/Catch代码块封装到一个类中 

为了恢复可读性,我们需要重构代码以引入一个新类。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;
        }
    }
}
  1.  创建一个单独的record对象。
  2. 重用它。

Trying With Lombok

项目Lombok是一个编译时注释处理器,它会生成额外的字节码。使用正确的注释就可以得到结果,而无需编写样板代码。

  • Project Lombok是一个Java库,它可以自动插入到您的编辑器和构建工具中,为您的Java增添风味。再也不要写getter或equals方法了,只需一个注释,你的类就有了功能齐全的构建器,自动化你的日志记录变量等等。

Lombok提供了@SneakyThrow注释:它允许抛出已检异常,而无需在方法签名中声明它们。然而,目前它并不适用于现有的API。

如果你是Lombok用户,请注意,状态停放有一个打开的GitHub问题。

Commons Lang 的救援

Apache Commons Lang是一个古老的项目。它在当时很流行,因为它提供了一些本可以成为Java API一部分的实用程序,但却没有。这比在每个项目中重新发明你的DateUtils和StringUtils要好得多。在研究这篇文章时,我发现它仍然使用很棒的api定期维护。其中之一是可失败的API。 

该API由两部分组成:

  1. 一个包装器Stream
  2. 签名接受异常的管道方法

这是一小段摘录:

Lambda表达式中的异常_第1张图片

代码最终变成了我们从一开始就期望的样子:

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语言的库:

  • Vavr core是一个Java函数式库。它有助于减少代码量并提高健壮性。迈向函数式编程的第一步是从不可变值开始思考。Vavr提供了不可变集合以及操作这些值所需的函数和控制结构。结果是美丽的,只是工作。

假设我们需要一个管道来收集异常和类。下面是该API的一个片段,描述了几个构建块:

Lambda表达式中的异常_第2张图片

它翻译成以下代码: 

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());
          }
      });
  1. 将这个调用封装到一个Vavr Try中。
  2. 将Try转换为Either,保留异常。如果我们不感兴趣,可以使用Optional对象。
  3. 根据Either是否包含异常(左)或预期结果(右)来采取行动。 

到目前为止,我们都在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
  1. 将Either的流划分为两个流组成的元组。
  2. 将左边的流从任一流压平,变成可抛掷的流。
  3. 将右边的stream从Either的stream展平为Class的stream。
  4. 想做什么就做什么。 

结论

Java最初的设计大量使用了已检异常。编程语言的发展证明了这不是一个好主意。

Java流不能很好地处理已检异常。将后者集成到前者所需的代码看起来并不好。为了恢复流的可读性,我们可以使用Apache Commons Lang。

编译只是问题的一小部分。我们通常希望对异常采取行动,而不是停止管道或忽略异常。在这种情况下,我们可以利用Vavr库,它提供了一种更函数式的方法。

你可能感兴趣的:(java,kotlin,开发语言)