1. 概述

Java 8 引入的 Lambda 表达式极大地简化了函数式编程,提供了一种简洁表达行为的方式。然而,JDK 提供的函数式接口在异常处理方面表现不佳——当需要处理异常时,代码会变得冗长且笨重。

本文将探讨在编写 Lambda 表达式时处理异常的几种方法。

2. 处理非受检异常

首先通过示例理解问题。假设有一个 List<Integer>,我们想用常数 50 除以列表中的每个元素并打印结果:

List<Integer> integers = Arrays.asList(3, 9, 7, 6, 10, 20);
integers.forEach(i -> System.out.println(50 / i));

这段代码能运行,但存在隐患:如果列表中包含元素 0,就会抛出 ArithmeticException: / by zero。用传统 try-catch 块修复这个问题,记录异常后继续执行后续元素:

List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> {
    try {
        System.out.println(50 / i);
    } catch (ArithmeticException e) {
        System.err.println(
          "Arithmetic Exception occured : " + e.getMessage());
    }
});

虽然解决了问题,但牺牲了 Lambda 表达式的简洁性——它不再是一个轻量级函数。为了解决这个问题,可以编写Lambda 包装器

static Consumer<Integer> lambdaWrapper(Consumer<Integer> consumer) {
    return i -> {
        try {
            consumer.accept(i);
        } catch (ArithmeticException e) {
            System.err.println(
              "Arithmetic Exception occured : " + e.getMessage());
        }
    };
}
List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(lambdaWrapper(i -> System.out.println(50 / i)));

我们创建了一个包装方法处理异常,然后将 Lambda 表达式作为参数传入。虽然有效,但你可能认为这只是把 try-catch 移到了另一个方法,实际代码量并未减少。确实如此,但我们可以用泛型改进它,使其适用于更多场景:

static <T, E extends Exception> Consumer<T>
  consumerWrapper(Consumer<T> consumer, Class<E> clazz) {
 
    return i -> {
        try {
            consumer.accept(i);
        } catch (Exception ex) {
            try {
                E exCast = clazz.cast(ex);
                System.err.println(
                  "Exception occured : " + exCast.getMessage());
            } catch (ClassCastException ccEx) {
                throw ex;
            }
        }
    };
}
List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(
  consumerWrapper(
    i -> System.out.println(50 / i), 
    ArithmeticException.class));

改进后的包装方法接受两个参数:Lambda 表达式和要捕获的异常类型。它不仅能处理任意数据类型(不限于 Integer),还能精确捕获特定异常而非超类 Exception。注意方法名从 lambdaWrapper 改为 consumerWrapper,因为它专门处理 Consumer 类型的函数式接口。类似地,我们可以为 FunctionBiFunctionBiConsumer 等编写包装方法。

3. 处理受检异常

修改前例:将结果写入文件而非控制台:

static void writeToFile(Integer integer) throws IOException {
    // 写入文件的逻辑,可能抛出 IOException
}

注意此方法声明了 throws IOException

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

编译时直接报错:

java.lang.Error: Unresolved compilation problem: Unhandled exception type IOException

因为 IOException受检异常,必须显式处理。有两种选择:

  1. 将异常抛出方法外,由调用方处理
  2. 在 Lambda 表达式内部处理

下面分别探讨这两种方案。

3.1 从 Lambda 表达式抛出受检异常

尝试在 main 方法声明 throws IOException

public static void main(String[] args) throws IOException {
    List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
    integers.forEach(i -> writeToFile(i));
}

仍然报相同的编译错误:

java.lang.Error: Unresolved compilation problem: Unhandled exception type IOException

这是因为 Lambda 表达式类似于匿名内部类。本例中,writeToFile 实现了 Consumer<Integer> 函数式接口。查看 Consumer 定义:

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

accept 方法未声明任何受检异常,因此 writeToFile 不能抛出 IOException。最直接的方案是用 try-catch 包装,将受检异常转为非受检异常后抛出:

List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> {
    try {
        writeToFile(i);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
});

虽然能编译运行,但代码又变得冗长。我们可以做得更好。创建一个自定义函数式接口,其 accept 方法允许抛出异常:

@FunctionalInterface
public interface ThrowingConsumer<T, E extends Exception> {
    void accept(T t) throws E;
}

然后实现一个能重新抛出异常的包装器:

static <T> Consumer<T> throwingConsumerWrapper(
  ThrowingConsumer<T, Exception> throwingConsumer) {
 
    return i -> {
        try {
            throwingConsumer.accept(i);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    };
}

最终简化 writeToFile 的调用方式:

List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(throwingConsumerWrapper(i -> writeToFile(i)));

这仍是一种变通方案,但最终代码简洁且易于维护ThrowingConsumerthrowingConsumerWrapper 都是泛型的,可在应用中复用。

3.2 在 Lambda 表达式中处理受检异常

最后修改包装器,使其能处理受检异常。由于 ThrowingConsumer 使用了泛型,可以轻松处理特定异常:

static <T, E extends Exception> Consumer<T> handlingConsumerWrapper(
  ThrowingConsumer<T, E> throwingConsumer, Class<E> exceptionClass) {
 
    return i -> {
        try {
            throwingConsumer.accept(i);
        } catch (Exception ex) {
            try {
                E exCast = exceptionClass.cast(ex);
                System.err.println(
                  "Exception occured : " + exCast.getMessage());
            } catch (ClassCastException ccEx) {
                throw new RuntimeException(ex);
            }
        }
    };
}

实际使用方式:

List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(handlingConsumerWrapper(
  i -> writeToFile(i), IOException.class));

注意:上述代码**仅处理 IOException**,其他类型的异常会被包装为 RuntimeException 抛出。

4. 总结

本文展示了如何通过包装方法在 Lambda 表达式中处理特定异常,同时保持代码简洁。我们还学习了如何为 JDK 的函数式接口编写抛出异常的替代方案,以处理或抛出受检异常。

另一种方案是探索偷抛异常的技巧

函数式接口和包装方法的完整源码可从此处下载,测试类在GitHub

如果需要现成的解决方案,ThrowingFunction项目值得参考。