简介

自从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