写点什么

还在 stream 中使用 peek? 不要被这些陷阱绊住了

作者:程序那些事
  • 2023-03-21
    广东
  • 本文字数:2414 字

    阅读完需:约 8 分钟

简介

自从 JDK 中引入了 stream 之后,仿佛一切都变得很简单,根据 stream 提供的各种方法,如 map,peek,flatmap 等等,让我们的编程变得更美好。


事实上,我也经常在项目中看到有些小伙伴会经常使用 peek 来进行一些业务逻辑处理。


那么既然 JDK 文档中说 peek 方法主要是在调试的情况下使用,那么 peek 一定存在着某些不为人知的缺点。一起来看看吧。

peek 的定义和基本使用

先来看看 peek 的定义:


    Stream<T> peek(Consumer<? super T> action);
复制代码


peek 方法接受一个 Consumer 参数,返回一个 Stream 结果。


而 Consumer 是一个 FunctionalInterface,它需要实现的方法是下面这个:


    void accept(T t);
复制代码


accept 对传入的参数 T 进行处理,但是并不返回任何结果。


我们先来看下 peek 的基本使用:


    public static void peekOne(){        Stream.of(1, 2, 3)                .peek(e -> log.info(String.valueOf(e)))                .toList();    }
复制代码


运行上面的代码,我们可以得到:


[main] INFO com.flydean.Main - 1[main] INFO com.flydean.Main - 2[main] INFO com.flydean.Main - 3
复制代码


逻辑很简单,就是打印出 Stream 中的元素而已。

peek 的流式处理

peek 作为 stream 的一个方法,当然是流式处理的。接下来我们用一个具体的例子来说明流式处理具体是如何操作的。


    public static void peekForEach(){        Stream.of(1, 2, 3)                .peek(e -> log.info(String.valueOf(e)))                .forEach(e->log.info("forEach"+e));    }
复制代码


这一次我们把 toList 方法替换成了 forEach,通过具体的打印日志来看看到底发生了什么。


[main] INFO com.flydean.Main - 1[main] INFO com.flydean.Main - forEach1[main] INFO com.flydean.Main - 2[main] INFO com.flydean.Main - forEach2[main] INFO com.flydean.Main - 3[main] INFO com.flydean.Main - forEach3
复制代码


通过日志,我们可以看出,流式处理的流程是对应流中的每一个元素,分别经历了 peek 和 forEach 操作。而不是先把所有的元素都 peek 过后再进行 forEach。

Stream 的懒执行策略

之所有会有流式操作,就是因为可能要处理的数据比较多,无法一次性加载到内存中。


所以为了优化 stream 的链式调用的效率,stream 提供了一个懒加载的策略。


什么是懒加载呢?


就是说 stream 的方法中,除了部分 terminal operation 之外,其他的都是 intermediate operation.


比如 count,toList 这些就是 terminal operation。当接受到这些方法的时候,整个 stream 链条就要执行了。


而 peek 和 map 这些操作就是 intermediate operation。


intermediate operation 的特点是立即返回,如果最后没有以 terminal operation 结束,intermediate operation 实际上是不会执行的。


我们来看个具体的例子:


    public static void peekLazy(){        Stream.of(1, 2, 3)                .peek(e -> log.info(String.valueOf(e)));    }
复制代码


运行之后你会发现,什么输出都没有。


这表示 peek 中的逻辑并没有被调用,所以这种情况大家一定要注意。

peek 为什么只被推荐在 debug 中使用

如果你阅读过 peek 的文档,你可能会发现 peek 是只被推荐在 debug 中使用的,为什么呢?


JDK 中的原话是这样说的:


In cases where the stream implementation is able to optimize away the production of some or all the elements (such as with short-circuiting operations like findFirst, or in the example described in count), the action will not be invoked for those elements.
复制代码


翻译过来的意思就是,因为 stream 的不同实现对实现方式进行了优化,所以不能够保证 peek 中的逻辑一定会被调用。


我们再来举个例子:


    public static void peekNotExecute(){        Stream.of(1, 2, 3)                .peek(e -> log.info("peekNotExecute"+e))                .count();    }
复制代码


这里的 terminal operation 是 count,表示对 stream 中的元素进行统计。


因为 peek 方法中参数是一个 Consumer,它不会对 stream 中元素的个数产生影响,所以最后的运行结果就是 3。


peek 中的日志输出并没有打印出来,表示 peek 没有被执行。


所以,我们在使用 peek 的时候,一定要注意 peek 方法是否会被优化。要不然就会成为一个隐藏很深的 bug。

peek 和 map 的区别

好了,讲到这里,大家应该对 peek 有了一个全面的认识了。但是 stream 中还有一个和 peek 类似的方法叫做 map。他们有什么区别呢?


前面我们讲到了 peek 方法需要的参数是 Consumer,而 map 方法需要的参数是一个 Function:


    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
复制代码


Function 也是一个 FunctionalInterface,这个接口需要实现下面的方法:


    R apply(T t);
复制代码


可以看出 apply 方法实际上是有返回值的,这跟 Consumer 是不同的。所以一般来说 map 是用来修改 stream 中具体元素的。 而 peek 则没有这个功能。


peek 方法接收一个 Consumer 的入参. 了解λ表达式的应该明白 Consumer 的实现类应该只有一个方法,该方法返回类型为 void. 它只是对 Stream 中的元素进行某些操作,但是操作之后的数据并不返回到 Stream 中,所以 Stream 中的元素还是原来的元素.


map 方法接收一个 Function 作为入参. Function 是有返回值的, 这就表示 map 对 Stream 中的元素的操作结果都会返回到 Stream 中去.


  • 要注意的是,peek 对一个对象进行操作的时候,虽然对象不变,但是可以改变对象里面的值。


大家可以运行下面的例子:


    public static void peekUnModified(){        Stream.of(1, 2, 3)                .peek(e -> e=e+1)                .forEach(e->log.info("peek unModified"+e));    }
public static void mapModified(){ Stream.of(1, 2, 3) .map(e -> e=e+1) .forEach(e->log.info("map modified"+e)); }
复制代码

总结

以上就是对 peek 的总结啦,大家在使用的时候一定要注意存在的诸多陷阱。


本文的例子https://github.com/ddean2009/learn-java-base-9-to-20/tree/master/peek-and-map/


更多文章请看 www.flydean.com

发布于: 2023-03-21阅读数: 57
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020-06-07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
还在stream中使用peek?不要被这些陷阱绊住了_Java_程序那些事_InfoQ写作社区