1. 简介

JDK提供的函数式接口(Functional Interfaces)对受检异常(checked exceptions)的处理并不完善。关于这个问题的更多细节,可以参考这篇文章

本文将探讨如何使用Vavr这个Java函数式库,优雅地解决Lambda表达式中的异常处理问题。关于Vavr的更多信息和配置方法,可以参考这篇文章

2. 使用CheckedFunction

Vavr提供了支持受检异常的函数式接口,包括CheckedFunction0CheckedFunction1直到CheckedFunction8。函数名末尾的数字(0,1,...,8)表示该函数接受的参数个数。

看个例子:

static Integer readFromFile(Integer integer) throws IOException {
    // 读取文件的逻辑,可能抛出IOException
}

我们可以在Lambda表达式中直接使用这个方法,无需处理IOException

List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);

CheckedFunction1<Integer, Integer> readFunction = i -> readFromFile(i);
integers.stream()
 .map(readFunction.unchecked());

如你所见,无需传统的try-catch或包装方法,就能在Lambda中调用可能抛出异常的方法。

⚠️ 踩坑提醒:在Stream API中使用此特性要格外小心,因为异常会立即终止整个流操作,导致剩余元素无法处理。

3. 使用辅助方法

Vavr的API类为上一节的场景提供了快捷方法:

List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);

integers.stream()
  .map(API.unchecked(i -> readFromFile(i)));

4. 使用Lifting(提升)

要优雅地处理IOException,可以在Lambda中写标准的try-catch块,但这会破坏Lambda的简洁性。Vavr的lifting(提升)特性可以解决这个问题。

Lifting是函数式编程的概念:将偏函数(partial function)提升为返回Option结果的全函数(total function)。

  • 偏函数:只在定义域的子集上有定义的函数,输入超出范围时会抛出异常
  • 全函数:在整个定义域上都有定义的函数

重写之前的例子:

List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
 
integers.stream()
  .map(CheckedFunction1.lift(i -> readFromFile(i)))
  .map(k -> k.getOrElse(-1));

注意:提升后的函数返回Option,异常发生时结果为Option.NonegetOrElse()方法在遇到Option.None时会返回指定的默认值。

5. 使用Try容器

虽然上一节的lift()避免了程序突然终止,但它会吞掉异常。调用方无法知道默认值是由什么异常导致的。这时可以使用Try容器。

Try是一个特殊容器,用于封装可能抛出异常的操作。如果操作失败,Try对象会表示为Failure并包裹异常。

看代码示例:

List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.stream()
  .map(CheckedFunction1.liftTry(i -> readFromFile(i)))
  .flatMap(Value::toJavaStream)
  .forEach(i -> processValidValue(i));

关于Try容器的更多用法,可以参考这篇文章

6. 总结

本文展示了如何使用Vavr库的特性,优雅地处理Lambda表达式中的异常问题。

虽然这些特性让异常处理更简洁,但使用时需格外谨慎。某些方法可能会让调用方遇到未声明的受检异常,造成意外。

本文所有示例的完整源代码可在GitHub获取。


原始标题:Exceptions in Lambda Expression Using Vavr