在java8的Stream中使用lambda表达式编程,如下:
static void test0() throws Exception {
long count = Files.walk(Paths.get("E:/ws_test"))// 获得项目目录下的所有目录及文件
.filter(file -> !Files.isDirectory(file)) // 筛选出文件
.filter(file -> file.toString().endsWith(".java"))// 筛选出 java 文件
.flatMap(file -> Files.lines(file)) // 按行获得文件中的文本
.filter(line -> !line.trim().isEmpty()) // 过滤掉空行
.count();
System.out.println("代码行数:" + count);
}
补充:
- Files.walk(Path) 在 JDK1.8 时添加,深度优先遍历一个 Path (目录),返回这个目录下所有的Path(目录和文件),通过 Stream<Path> 返回;
- Files.lines(Path) 也是在 JDK1.8 时添加,功能是返回指定Path(文件)中所有的行,通过 Stream<String> 返回;
上述代码编译报错,因为 Files.lines(Path) 会抛出 IOException。及时将上述代码修改成如下,仍然编译报错:
static void test0() throws Exception {
try{
long count = Files.walk(Paths.get("E:/ws_test"))// 获得项目目录下的所有目录及文件
.filter(file -> !Files.isDirectory(file)) // 筛选出文件
.filter(file -> file.toString().endsWith(".java"))// 筛选出 java 文件
.flatMap(file -> Files.lines(file)) // 按行获得文件中的文本
.filter(line -> !line.trim().isEmpty()) // 过滤掉空行
.count();
System.out.println("代码行数:" + count);
} catch(Exception e){
}
}
原因是:在JDK类中,lambda不在您编写的位置评估,而是在某些完全不相关的位置评估。修改成如下代码就可编译通过:
static void test1() throws Exception {
long count = Files.walk(Paths.get("E:/ws_test"))// 获得项目目录下的所有目录及文件
.filter(file -> !Files.isDirectory(file)) // 筛选出文件
.filter(file -> file.toString().endsWith(".java"))// 筛选出 java 文件
.flatMap(file -> {
try {
return Files.lines(file);
} catch (IOException e) {
return Stream.empty();
}
}) // 按行获得文件中的文本
.filter(line -> !line.trim().isEmpty()) // 过滤掉空行
.count();
System.out.println("代码行数:" + count);
}
虽然可以编译、运行通过,但是这样写法不够简洁,而且不符合lambda的one-liner expression
解决方法一:
新建一个方法,在其中处理各种异常,getLines 方法中抛出异常时我们输出了异常,并返回一个空的 Stream:
static void test2() throws IOException {
long count = Files.walk(Paths.get("E:/ws_test")) // 获得项目目录下的所有文件
.filter(file -> !Files.isDirectory(file)) // 筛选出文件
.filter(file -> file.toString().endsWith(".java")) // 筛选出 java 文件
.flatMap(file -> getLines(file)) // 按行获得文件中的文本
.filter(line -> !line.trim().isEmpty()) // 过滤掉空行
.count();
System.out.println("代码行数:" + count);
}
private static Stream<String> getLines(Path file) {
try {
return Files.lines(file);
} catch (IOException ex) {
ex.printStackTrace(System.err);
return Stream.empty();
}
}
这种方法和最上面的try-catch一样,不具有通用性。
方法二:
将会抛出异常的函数进行包装,使其不抛出受检异常。
如果一个 FunctionInterface 的方法会抛出受检异常(比如 Exception),那么该 FunctionInterface 便可以作为会抛出受检异常的 Lambda 的目标类型。
我们定义如下一个受检的 FunctionInterface:
@FunctionalInterface
interface CheckedFunction<T, R> {
R apply(T t) throws Throwable;
}
那么该 FunctionalInterface 便可以作为类似于file -> File.lines(file) 这类会抛出受检异常的 Lambda 的目标类型,此时 Lambda 中并不需要捕获异常(因为目标类型的 apply 方法已经将异常抛出了)。
之所以原来的 Lambda 需要捕获异常,就是因为在流式操作 flatMap 中使用的 java.util.function 包下的 Function<T, R> 没有抛出异常:
那我们如何使用 CheckedFunction 到流式操作的 Lambda 中呢?
首先我们定义一个 Attempt 接口,它的 apply 静态方法提供将 CheckedFunction 包装为 Function 的功能:
public interface Attempt {
static <T, R> Function<T, R> apply(CheckedFunction<T, R> function) {
Objects.requireNonNull(function);//如果为空会抛nullpoint异常
return t -> {
try {
return function.apply(t);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
};
}
}
然后在原先的代码中,我们使用 Attempt.apply 方法来对会抛出受检异常的 Lambda 进行包装:
static void test3() throws IOException {
long count = Files.walk(Paths.get("E:/ws_test")) // 获得项目目录下的所有文件
.filter(file -> !Files.isDirectory(file)) // 筛选出文件
.filter(file -> file.toString().endsWith(".java")) // 筛选出 java 文件
.flatMap(Attempt.apply(file -> Files.lines(file))) // 按行获得文件中的文本
.filter(line -> !line.trim().isEmpty()) // 过滤掉空行
.count();
System.out.println("代码行数:" + count);
}
此时,我们便可以选择是否去捕获异常(RuntimeException)。这种解决方法下,我们一般不关心抛出异常的情况 —— 比如自己写的小例子,抛出了异常程序就该终止;或者你知道这个 Lambda 确实 100% 不会抛出异常。
不过我更倾向于抛出异常时,我们来指定处理的方式:
public interface Attempt {
static <T, R> Function<T, R> apply(CheckedFunction<T, R> function) {
Objects.requireNonNull(function);//如果为空会抛nullpoint异常
return t -> {
try {
return function.apply(t);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
};
}
static <T, R> Function<T, R> apply(CheckedFunction<T, R> function,Function<Throwable, R> handler) {
Objects.requireNonNull(function);
Objects.requireNonNull(handler);
return t -> {
try {
return function.apply(t);
} catch (Throwable e) {
return handler.apply(e);
}
};
}
}
比如我们前面的例子,如果 file -> Files.lines(file) 抛出异常了,说明在访问 file 类的时候出了问题,我们可以就假设这个文件的行数为 0 ,那么默认值就是个空的 Stream<String>(当然你也可以选择顺手记录一下异常):
static void test4() throws IOException {
long count = Files.walk(Paths.get("E:/ws_test")) // 获得项目目录下的所有文件
.filter(file -> !Files.isDirectory(file)) // 筛选出文件
.filter(file -> file.toString().endsWith(".java")) // 筛选出 java 文件
.flatMap(Attempt.apply(file -> Files.lines(file),ex -> Stream.empty())) // 按行获得文件中的文本
.filter(line -> !line.trim().isEmpty()) // 过滤掉空行
.count();
System.out.println("代码行数:" + count);
}
注意:使用 CheckedFunction这种方式更为通用 —— 类似的,我们可以包装:
- CheckedConsumer 为 java.util.function.Consumer,
- CheckedSupplier 为 java.util.function.Suppiler...
public interface Attempt {
......
/**
* 包装受检的 Consumer
*/
static <T> Consumer<T> accept(CheckedConsumer<T> consumer) {
Objects.requireNonNull(consumer);
return t -> {
try {
consumer.accept(t);
} catch (Throwable e) {
throw new RuntimeException(e);
}
};
}
/**
* 包装受检的 Consumer,并自定义异常处理
*/
static <T> Consumer<T> accept(CheckedConsumer<T> consumer, Consumer<Throwable> handler) {
Objects.requireNonNull(consumer);
Objects.requireNonNull(handler);
return t -> {
try {
consumer.accept(t);
} catch (Throwable e) {
handler.accept(e);
}
};
}
}