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
类型的函数式接口。类似地,我们可以为 Function
、BiFunction
、BiConsumer
等编写包装方法。
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
是受检异常,必须显式处理。有两种选择:
- 将异常抛出方法外,由调用方处理
- 在 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)));
这仍是一种变通方案,但最终代码简洁且易于维护。ThrowingConsumer
和 throwingConsumerWrapper
都是泛型的,可在应用中复用。
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项目值得参考。