5.9 利用提取的方法实现异常处理
问题
lambda 表达式中的代码需要抛出异常,但用户不希望将 lambda 代码块与异常处理的代码混在一起。
方案
创建一个单独的方法来执行操作、处理异常,在 lambda 表达式中调用提取的方法。
讨论
lambda 表达式实际上属于函数式接口中单一抽象方法的实现。与匿名内部类一样,lambda 表达式只能抛出在抽象方法签名中声明的异常。
如果所需的异常为非受检异常(unchecked exception),则情况相对简单。所有非受检异常均源自 java.lang.RuntimeException 类 10。与其他 Java 代码类似,lambda 表达式可以抛出运行时异常(runtime exception)而不必进行声明或将代码包装在 try/catch 块中,异常随后被传送给调用者。
10这可能是整个 Java API 中最糟糕的命名——既然所有异常均在运行时抛出(否则它们属于编译器错误),那么将这个类命名为 UncheckedException 不是更直接吗?或许是为了凸显这个命名的糟糕,Java 8 引入了一个称为 java.io.UncheckedIOException 的类,以规避本范例讨论的某些问题。
例 5-31 创建了一个名为 div 的方法,它采用常数值对集合中所有元素进行除法操作。
例 5-31 可能抛出非受检异常的 lambda 表达式
- public List<Integer> div(List<Integer> values, Integer factor) {
- return values.stream()
- .map(n -> n / factor) ➊
- .collect(Collectors.toList());
- }
➊ 可能抛出 ArithmeticException
对整数除法而言,除数为 0 将抛出 ArithmeticException(非受检异常)11,它将传送给调用者,如例 5-32 所示。
11有趣的是,如果将被除数和除数的类型从 Integer 改为 Double,那么即便除数为 0.0,程序也不会抛出任何异常,而是输出一个所有元素为“无穷大”(Infinity)的结果。无论读者是否相信,根据二进制计算机处理浮点数需要遵循的 IEEE 754 规范,这种操作是合法的(作者在使用 Fortran 语言编程时曾被这种情况搞得十分头疼,花了不少时间才摆脱这个噩梦)。
例 5-32 客户端代码
List<Integer> values = Arrays.asList(30, 10, 40, 10, 50, 90);List<Integer> scaled = demo.div(values, 10);System.out.println(scaled);// 打印[3, 1, 4, 1, 5, 9]scaled = demo.div(values, 0);System.out.println(scaled);// 抛出ArithmeticException(因为除数为0)
客户端代码调用 div 方法,如果除数为 0,lambda 表达式将抛出 ArithmeticException。客户端可以在 map 方法中添加一个 try/catch 块以处理异常,但代码的可读性会因此而变得很差(如例 5-33 所示)。
例 5-33 包含 try/catch 代码块的 lambda 表达式
- public List<Integer> div(List<Integer> values, Integer factor) {
- return values.stream()
- .map( n -> {
- try {
- return n / factor;
- } catch (ArithmeticException e) {
- e.printStackTrace();
- }
- })
- .collect(Collectors.toList());
- }
只要在函数式接口中声明受检异常(checked exception),就能应用上述过程。
一般来说,应尽量保持流处理代码的简洁,让每个中间操作占据一行。我们可以将 map 方法内部的函数提取到另一个方法以简化代码,然后调用这个方法来完成流处理,如例 5-34 所示。
例 5-34 将 lambda 表达式提取到另一个方法
- private Integer divide(Integer value, Integer factor) {
- try {
- return value / factor;
- } catch (ArithmeticException e) { ➊
- e.printStackTrace();
- }
- }
- public List<Integer> divUsingMethod(List<Integer> values, Integer factor) {
- return values.stream()
- .map(n -> divide(n, factor)) ➋
- .collect(Collectors.toList());
- }
❶ 这段代码负责处理异常
❷ 流处理代码得以简化
此外,如果提取的方法不需要 factor 值,则 map 方法的参数可以简化为一个方法引用。
将 lambda 表达式提取到单独的方法同样具有不少优点。我们既可以为提取的方法编写测试(对于私有方法则使用反射),也可以在方法内部设置断点,还可以使用与方法相关的其他机制。
另见
有关受检异常与 lambda 表达式的讨论请参见范例 5.10,有关利用泛型包装器处理异常的讨论请参见范例 5.11。
