1. 简介
JDK提供的函数式接口(Functional Interfaces)对受检异常(checked exceptions)的处理并不完善。关于这个问题的更多细节,可以参考这篇文章。
本文将探讨如何使用Vavr这个Java函数式库,优雅地解决Lambda表达式中的异常处理问题。关于Vavr的更多信息和配置方法,可以参考这篇文章。
2. 使用CheckedFunction
Vavr提供了支持受检异常的函数式接口,包括CheckedFunction0
、CheckedFunction1
直到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.None
。getOrElse()
方法在遇到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获取。