Lambda 表达式在编码时带来了极简的体验,但是当 Lambda 表达式中出现 Checked Exception (受检异常)时,为了要处理异常,整个 Lambda 表达式就会变冗长,与 Lambda 表达式本身的风格格格不入,本文将讨论几种可能的解决方案。

一块简单的统计文件夹下文本行数的代码:

1
2
3
4
5
6
long count = Files.walk(Paths.get("D:/Test"))                      // 获得项目目录下的所有目录及文件
.filter(file -> !Files.isDirectory(file)) // 筛选出文件
.filter(file -> file.toString().endsWith(".java")) // 筛选出 java 文件
.flatMap(file -> Files.lines(file)) // 按行获得文件中的文本
.filter(line -> !line.trim().isEmpty()) // 过滤掉空行
.count();

显然,由于 Files.lines(file) 会抛出受检异常 IOException,当代码敲到这一行时,Idea 无情地给这一行代码加上了红色波浪线——未处理的受检异常,要让代码可以编译成功,要这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
long count = Files.walk(Paths.get("D:/Test"))                      // 获得项目目录下的所有文件
.filter(file -> !Files.isDirectory(file)) // 筛选出文件
.filter(file -> file.toString().endsWith(".java")) // 筛选出 java 文件
.flatMap(file -> {
try {
return Files.lines(file);
} catch (IOException ex) {
ex.printStackTrace(System.err);
return Stream.empty(); // 抛出异常时返回一个空的 Stream
}
}) // 按行获得文件中的文本
.filter(line -> !line.trim().isEmpty()) // 过滤掉空行
.count();

Lambda 代码中这么大一块异常处理显得尤其突兀,如果在整个 Stream 操作的过程中再多几个这样需要处理异常的情况,那真是把 Lambda 的简洁性丢得一干二净。

解决方法1

既然是因为 Files.lines(file) 的异常没有处理才引发的问题,那就写个方法,将没有处理异常的方法包装起来,然后在方法内部处理异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    long count = Files.walk(Paths.get("D:/Test"))                       // 获得项目目录下的所有文件
.filter(file -> !Files.isDirectory(file)) // 筛选出文件
.filter(file -> file.toString().endsWith(".java")) // 筛选出 java 文件

.flatMap(file -> getLines(file)) // 按行获得文件中的文本

.filter(line -> !line.trim().isEmpty()) // 过滤掉空行
.count();

private static Stream<String> getLines(Path file) {
try {
return Files.lines(file);
} catch (IOException ex) {
ex.printStackTrace(System.err);
return Stream.empty();
}
}

这种方法下,我们需要处理受检异常 —— 即在程序抛出异常的时候,我们需要告诉程序怎么去做(getLines 方法中抛出异常时我们输出了异常,并返回一个空的 Stream)

解决方法2

如果一个 FunctionInterface 的方法会抛出受检异常(比如 Exception),那么该 FunctionInterface 便可以作为会抛出受检异常的 Lambda 的目标类型。(说人话:定义一个抛出异常的函数式接口替代原有的函数式接口)
定义如下一个 FunctionInterface:

1
2
3
4
@FunctionalInterface
interface UncheckedFunction<T, R> {
R apply(T t) throws Exception;
}

那么该 FunctionInterface 便可以作为类似于 file -> File.lines(file) 这类会抛出受检异常的 Lambda 的目标类型,此时 Lambda 中并不需要捕获异常(因为目标类型的 apply 方法已经将异常抛出了)—— 之所以原来的 Lambda 需要捕获异常,就是因为在流式操作 flatMap 中使用的 java.util.function 包下的 Function<T, R> 没有抛出异常。

再定义一个 Try 类,它的 of 方法提供将 UncheckedFunction 包装为 Function 的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Try {

public static <T, R> Function<T, R> of(UncheckedFunction<T, R> mapper) {
Objects.requireNonNull(mapper);
return t -> {
try {
return mapper.apply(t);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
};
}

@FunctionalInterface
public static interface UncheckedFunction<T, R> {

R apply(T t) throws Exception;
}
}

在原先的代码中,使用 Try.of 方法来对会抛出受检异常的 Lambda 进行包装:

1
2
3
4
5
6
7
8
long count = Files.walk(Paths.get("D:/Test"))              // 获得项目目录下的所有文件
.filter(file -> !Files.isDirectory(file)) // 筛选出文件
.filter(file -> file.toString().endsWith(".java")) // 筛选出 java 文件

.flatMap(Try.of(file -> Files.lines(file))) // 将 会抛出受检异常的 Lambda 包装为 抛出非受检异常的 Lambda

.filter(line -> !line.trim().isEmpty()) // 过滤掉空行
.count();

这种解决方法下,我们一般不关心抛出异常的情况 —— 比如自己写的小例子,抛出了异常程序就该终止;或者你知道这个 Lambda 确实 100% 不会抛出异常。

从模块整体对外表现的稳定性来看,在函数式接口的参数处指定一个默认值,在出错时返回一个默认值可能是更好的解决方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Try {

public static <T, R> Function<T, R> of(
UncheckedFunction<T, R> mapper, R defaultR) {
Objects.requireNonNull(mapper);
return t -> {
try {
return mapper.apply(t);
} catch (Exception ex) {
System.err.println(ex.getMessage());
return defaultR;
}
};
}

@FunctionalInterface
public static interface UncheckedFunction<T, R> {

R apply(T t) throws Exception;
}
}

针对统计文本行数的例子,在访问文件出错时,不妨使用“鸵鸟算法”,假装这个文件不存在,让其返回默认值:一个空的 Stream 对象。

小结

使用 UncheckedFunction 这种方式更为通用,我们可以在更多的地方将 UncheckedFunction 包装成 java.util.function.Function。类似的,我们可以包装 UncheckedConsumer 为 java.util.function.Consumer,包装 UncheckedSupplier 为 Suppiler,UncheckedBiFunction 为 BiFunction 等。

参考资料