写点什么

可能是 Java Stream 的最佳实践(三)

用户头像
ES_her0
关注
发布于: 2021 年 02 月 27 日

前两篇文章向大家简要地介绍了 Stream 所具备的能力,有些可能你非常熟悉,有些可能你没用过但其实很有用,不管如何,希望能对大家的日常开发有所帮助。下面总结了一些实际开发中可能被大家忽略的点,但会有效地提升代码的可读性和性能。

一个操作一行

可读性对于业务代码来说十分重要,有些时候甚至可以牺牲一点性能。stream 大部分时候都是链式操作,如果不注意分隔,加上有些函数可能还比较长,操作符比较多,会给阅读者带来很多麻烦。所以提倡在 stream 之后一个操作符一行,清晰明了。

//BadString s = Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1").parallel()        .filter(x -> x.startsWith("a")).collect(joining());
//GoodString s = Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1") .parallel() .filter(x -> x.startsWith("a")) .collect(joining());
复制代码

import static

这条还是和代码可读性相关的。stream 相关的方法可以静态引入,从而减少一些冗长的类名,减少视觉干扰。

//Badstrings.stream()	.sorted(Comparator.reverseOrder())	.limit(10)	.collect(Collectors.toMap(Function.identity(), String::length));
//Goodimport 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); // okstreamSupplier.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,这样就免去拆箱的过程,效率更高。

//GoodOptionalInt 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());
//output234514
复制代码

peek 接受一个 Consumer 作为参数,Consumer 的方法签名是接收一个值且返回 void,什么都不改变,什么也不返回。虽然把用作 debug 有点不厚道,但真的很好用。

总结

stream 最佳实践系列已经全部更新完成,从 stream 的基础开始,讲了 stream 的常用 API 及其能力,最后总结了一些日常开发过程的技巧实践。希望可以帮到正在阅读的你,如有问题,也请斧正。


发布于: 2021 年 02 月 27 日阅读数: 15
用户头像

ES_her0

关注

还未添加个人签名 2018.03.21 加入

还未添加个人简介

评论

发布
暂无评论
可能是Java Stream的最佳实践(三)