当前位置: 首页 > 后端技术 > Java

还在使用peekinstream吗?别被这些陷阱坑了

时间:2023-04-01 14:23:34 Java

简介自从在JDK中引入了stream之后,一切就显得非常简单了。根据stream提供的各种方法,比如map,peek,flatmap等,让我们的编程更好。其实我经常看到一些小伙伴经常在项目中使用peek来进行一些业务逻辑处理。所以既然JDK的文档说peek方法主要是用来调试的,那么peek肯定有一些不为人知的缺点。一起来看看吧。peek的定义和基本使用先来看看peek的定义:Streampeek(Consumeraction);peek方法接受一个Consumer参数并返回一个Stream结果。Consumer是一个FunctionalInterface,它需要实现的方法如下:voidaccept(Tt);accept处理传入的参数T,但不返回任何结果。先来看看peek的基本用法:publicstaticvoidpeekOne(){Stream.of(1,2,3).peek(e->log.info(String.valueOf(e))).toList();}运行上面的代码,我们可以得到:[main]INFOcom.flydean.Main-1[main]INFOcom.flydean.Main-2[main]INFOcom.flydean.Main-3逻辑很简单,就是仅打印出流中的元素。peek的流处理作为stream的一种方法,peek当然是流处理。下面我们通过一个具体的例子来说明流处理是如何工作的。publicstaticvoidpeekForEach(){Stream.of(1,2,3).peek(e->log.info(String.valueOf(e))).forEach(e->log.info("forEach"+e));这次我们用forEach替换toList方法,通过打印日志看看发生了什么。[main]INFOcom.flydean.Main-1[main]INFOcom.flydean.Main-forEach1[main]INFOcom.flydean.Main-2[main]INFOcom.flydean.Main-forEach2[main]INFOcom。flydean.Main-3[main]INFOcom.flydean.Main-forEach3通过日志可以看出流处理的过程对应流中的每一个元素,分别进行了peek和forEach操作。而不是先查看所有元素然后执行forEach。Stream所有的惰性执行策略都会有流式操作,因为可能要处理的数据很多,不能一次性加载到内存中。所以为了优化stream链调用的效率,stream提供了懒加载策略。什么是延迟加载?也就是说,在stream方法中,除了一些终端操作外,其他都是中间操作。例如,count和toList是终端操作。当接收到这些方法时,将执行整个流链。peek和map的操作是中间操作。中间操作的特点是立即返回。如果不是以终端操作结束,则中间操作不会真正执行。我们来看一个具体的例子:publicstaticvoidpeekLazy(){Stream.of(1,2,3).peek(e->log.info(String.valueOf(e)));}运行后,你会发现,没有任何输出。这意味着peek中的逻辑还没有被调用,所以你必须注意这种情况。为什么peek只推荐用于调试?如果你阅读peek的文档,你可能会发现peek只推荐用于调试。为什么?JDK中的原话是这样说的:在流实现能够优化掉部分或所有元素的生产的情况下(例如使用像findFirst这样的短路操作,或者在count中描述的示例中),Action将不会为这些元素调用。翻译过来的意思是因为stream的不同实现优化了实现,所以不能保证peek里面的逻辑一定会被调用。让我们再举一个例子:publicstaticvoidpeekNotExecute(){Stream.of(1,2,3).peek(e->log.info("peekNotExecute"+e)).count();}这里的终端操作是count,意思是对stream中的元素进行计数。因为peek方法中的参数是一个Consumer,不会影响流中的元素个数,所以最终运行结果为3。peek中的log输出没有打印出来,说明没有执行peek。所以我们在使用peek的时候一定要注意peek方法是否会被优化。否则会成为深藏不露的bug。peek和map之间的区别很好。至此,大家应该对peek有了一个全面的认识。但是还有一个类似于peek的方法叫mapinstream。它们之间有什么区别?前面我们提到peek方法需要的参数是Consumer,map方法需要的参数是Function:Streammap(Functionmapper);Function也是一个FunctionalInterface,这个接口需要实现如下方法:Rapply(Tt);可以看出apply方法其实是有返回值的,这点和Consumer是不一样的。所以一般来说,map是用来修改stream中的具体元素的。而peek没有这个功能。peek方法接收Consumer的输入参数。懂lambda表达式的应该明白,Consumer的实现类应该只有一个方法,这个方法的返回类型是void。它只是对Stream中的元素进行一定的操作,但是操作后的数据并没有返回给Stream,所以Stream中的元素还是原来的元素。map方法接受一个函数作为输入参数。Function是有返回值的,也就是说对Stream中元素的操作结果会返回给Stream。需要注意的是,peek对对象进行操作时,虽然对象保持不变,但是对象内部的值是可以的变了。您可以运行以下示例:publicstaticvoidpeekUnModified(){Stream.of(1,2,3).peek(e->e=e+1).forEach(e->log.info("peekunModified"+e));}publicstaticvoidmapModified(){Stream.of(1,2,3).map(e->e=e+1).forEach(e->log.info("mapmodified"+e));}总结以上是peek的总结。使用时一定要注意很多陷阱。本文示例https://github.com/ddean2009/learn-java-base-9-to-20/tree/master/peek-and-map/更多文章请看www.flydean.com