前两篇文章向大家简要地介绍了 Stream 所具备的能力,有些可能你非常熟悉,有些可能你没用过但其实很有用,不管如何,希望能对大家的日常开发有所帮助。下面总结了一些实际开发中可能被大家忽略的点,但会有效地提升代码的可读性和性能。
一个操作一行
可读性对于业务代码来说十分重要,有些时候甚至可以牺牲一点性能。stream 大部分时候都是链式操作,如果不注意分隔,加上有些函数可能还比较长,操作符比较多,会给阅读者带来很多麻烦。所以提倡在 stream 之后一个操作符一行,清晰明了。
//Bad
String s = Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1").parallel()
.filter(x -> x.startsWith("a")).collect(joining());
//Good
String s = Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1")
.parallel()
.filter(x -> x.startsWith("a"))
.collect(joining());
复制代码
import static
这条还是和代码可读性相关的。stream 相关的方法可以静态引入,从而减少一些冗长的类名,减少视觉干扰。
//Bad
strings.stream()
.sorted(Comparator.reverseOrder())
.limit(10)
.collect(Collectors.toMap(Function.identity(), String::length));
//Good
import static java.util.stream.Collectors.*;
strings.stream()
.sorted(reverseOrder())
.limit(10)
.collect(toMap(identity(), String::length));
复制代码
方法引用优于 lambda
方法引用说白了就是基于类型推断的 lambda 表达式,写法上有所区别。当一个函数的输入和输出都一致时(即方法签名一致),可以简化成方法引用的写法,以 String 的 length 方法为例:
x -> x.length
//等价于
String::length
复制代码
为什么上一个例子中filter(x -> x.startsWith("a"))
就用不了String::startsWith
呢,因为这里输入的参数是 x,而不是"a",无法达到判断的效果。如果真想这么写也可以:
public static boolean startWithA(String x){
return x.startsWith("a");
}
//这个类叫Test,这样就可以使用方法引用了
String s = Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1").parallel()
.filter(Test::startWithA).collect(joining());
复制代码
回正题,为什么推荐尽量使用方法引用呢?因为 lambda 表达式在编译时会被翻译成一个静态方法:
private static Integer lambda$main$0(String s) {
return s.length();
}
复制代码
但方法引用只会对应一个invokedynamic
的字节码命令,不会有额外的方法,处理效率比 lambda 表达式更高。
重用 Stream
Stream 是不可重用的,如果一个 stream 执行一个终端操作之后,再次执行的话就会报异常:stream has already been operated upon or closed。之前我们说过,stream 不是一个数据结构,不存储数据,一次性的。但如果你出于不想重复写代码的考虑,真的想重用也不是没有办法。
Supplier<Stream<String>> streamSupplier =
() -> Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> s.startsWith("a"));
streamSupplier.get().anyMatch(s -> true); // ok
streamSupplier.get().noneMatch(s -> true); // ok
复制代码
放到一个 Supplier 中,每次调用 get 都会构建一个新的 stream 出来,虽然效率上没有提升,但达到了Do not Repeat Youself
的目的,代码更干净。
注意使用顺序,先 filter 后处理
这是效率的问题,也是一个逻辑问题。肯定要先做过滤再做别的操作,不然所有的操作都会走一遍最后再过滤肯定相对较慢。如果有 filter 的需求,大部分就放在第一个(除非你需要过滤中间操作可能产生的 null)。就不举例子了。
注意使用 Null Check
这一步可以有效地减少 stream 操作过程中的 NPE,当你无法控制 stream 里面都有什么东西时,可以添加一步非空过滤,如下:
Optional<Integer> reduce = Stream.of(1, 2, 3, 4, 5, null)
.filter(Objects::nonNull)
.filter(x -> x > 1)
.reduce(Integer::sum);
复制代码
这一步校验的小技巧可以避免很多可能的异常,比较实用。
用不同的 stream 处理对应的原生类型
Stream 支持了原生类型的各种 stream,比如:IntStream
LongStream
DoubleStream
。如果处理的流是原生类型的数据,优先选择这些 stream,这样就免去拆箱的过程,效率更高。
//Good
OptionalInt reduce = IntStream.of(1, 2, 3, 4, 5)
.filter(x -> x > 1)
.reduce(Integer::sum);
//Bad:注意这里返回的 Optional<Integer>
Optional<Integer> reduce = Stream.of(1, 2, 3, 4, 5)
.filter(x -> x > 1)
.reduce(Integer::sum);
复制代码
异常处理
有时候在某个中间过程执行方法时,这个方法会向外抛一个受检异常,这个异常你必须要处理,作为 stream 是可以支持在中间处理的,但代码可能就会变成这样:
List<Class> classes =
Stream.of("java.lang.Object",
"java.lang.Integer",
"java.lang.String")
.map(className -> {
try {
return Class.forName(className);
}
catch (ClassNotFoundException e) {
// Ignore error
return null;
}
})
//这里要过滤转换失败带来的null,这里的filter可以在写后面
.filter(Objects::nonNull)
.collect(Collectors.toList());
复制代码
用一个 map 来处理异常,代码的视觉污染会十分严重,让人看了就没有去维护的欲望。但受检异常必须要处理,怎么办呢:
Class toClass(String className) {
try {
return Class.forName(className);
}
catch (ClassNotFoundException e) {
return null;
}
}
List<Class> classes =
Stream.of("java.lang.Object",
"java.lang.Integer",
"java.lang.String")
.map(this::toClass)
.filter(Objects::nonNull)
.collect(Collectors.toList());
复制代码
定义一个方法在外部,将受检异常"吃掉"。既处理了异常,也让代码变得可读可维护。
debug 技巧
stream 的 debug 比较困难,它不像正常代码那样执行一行是一行,像 IDEA 这种的工具可以提供 debug 工具,但也仅限于本地且按照了 IDEA 的情况下。有一个 API 却可以感知执行的步骤,而且属于中间操作,不像 foreach 那样是一个终端操作。依然使用上面的例子:
Optional<Integer> reduce = Stream.of(1, 2, 3, 4, 5, null)
.filter(Objects::nonNull)
.filter(x -> x > 1)
.peek(System.out::println)
.reduce(Integer::sum);
System.out.println(reduce.get());
//output
2
3
4
5
14
复制代码
peek 接受一个 Consumer 作为参数,Consumer 的方法签名是接收一个值且返回 void,什么都不改变,什么也不返回。虽然把用作 debug 有点不厚道,但真的很好用。
总结
stream 最佳实践系列已经全部更新完成,从 stream 的基础开始,讲了 stream 的常用 API 及其能力,最后总结了一些日常开发过程的技巧实践。希望可以帮到正在阅读的你,如有问题,也请斧正。
评论