共计 3144 个字符,预计需要花费 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