Java Stream中peek和map不为人知的秘密
今天开发时,有段代码如下,这里我开始用Java Stream 中的map
来修改对象的值
retPage.setRecords(retList.stream().map(questionPageVO -> {
questionPageVO.setCreateUserName(userIdAndUserMap.get(questionPageVO.getCreateId()).getUsername());
questionPageVO.setUpdateUserName(userIdAndUserMap.get(questionPageVO.getUpdateId()).getUsername());
return questionPageVO;
}).collect(Collectors.toList()));
但idea提示我这里可以替换为peek
,
替换之后的写法
retPage.setRecords(retList.stream().peek(questionPageVO -> {
questionPageVO.setCreateUserName(userIdAndUserMap.get(questionPageVO.getCreateId()).getUsername());
questionPageVO.setUpdateUserName(userIdAndUserMap.get(questionPageVO.getUpdateId()).getUsername());
}).collect(Collectors.toList()));
这样确实更简单整洁了,但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 baseUse() {
List<Integer> list = Stream.of(1,2,3)
.peek(System.out::println)
.collect(Collectors.toList());
System.out.println(list);
}
输出内容:
1
2
3
[1, 2, 3]
3. peek
的流式处理
public static void peekForEach() {
Stream.of(1,2,3)
.peek(System.out::println)
.forEach(e -> System.out.println("forEach:" + e));
}
输出内容:
1
forEach:1
2
forEach:2
3
forEach:3
通过输出内容也可以看出,流式处理流程,是对应流中每一个元素,分别经历peek
和forEach
操作(即一个元素执行完所有流程),而不是等peek
完所有元素元素后再执行forEach
坑一:Stream的懒执行策略
之所以有流操作,是因为有时候处理的数据比较多,无法一次性加载到内存中。
为了优化stream的链式调用效率,stream还提供了一个懒加载策略。
什么是懒加载呢?
懒加载也叫intermediate operation, 在stream
的方法中,大部分都是懒加载,另外部分则是terminal operation, 例如collect
、count
等,当有这种非懒加载的方法调用时,整个链式都会被执行,如开始的baseUse
示例。
但peek
和map
,都是懒加载方法,即intermediate operation。
intermediate operation的特点是立即返回,如果最后没有以terminal operation结束,intermediate operation实际上是不会执行的。
贴个官方解释图
让我们来看这个示例:
public static void peekLazy() {
Stream.of(1,2,3)
.peek(e -> System.out.println("peek lazy: " + e));
}
执行之后,结果什么都没输出,表示peek
中的逻辑没有被调用这里就是很大的一个坑,使用的时候要注意。
同理这里map
也是一样。
public static void mapLazy() {
Stream.of(1,2,3)
.map(e -> {
e = e+1;
System.out.println("map lazy: " + e);
return e;
});
}
坑二:可能不被调用
如果你读过peek源码,应该会记得这么一句话: This method exists mainly to support debugging, where you want to see the elements as they flow past a certain point in a pipeline.
意思是peek
推荐只在debug模式中使用,方便查看元素的一些信息。
先来看个示例:
public static void peekNotInvoke() {
Stream.of(1,2,3)
.peek(e -> System.out.println("peek invoke: " + e))
.count(Collectors.toList());
System.out.println("peekNotInvoke");
}
这里只输出了3, peek
中的内容并没有输出,这个结果和你预期的一致不?
看下这段源码,明确提示了,使用peek
时,有可能会被优化掉(即不调用)
但count
改为collect
则可以执行
Stream.of(1,2,3)
.peek(e -> System.out.println("peek invoke: " + e))
.collect(Collectors.toList());
System.out.println("peekInvoke");
所以,我们在使用peek
的时候,一定要注意peek
方法是否会被优化。要不然就会成为一个隐藏很深的bug。
同样的,map
也有这个问题。
peek和map的区别
正如前面提到,idea让我把map
换成了peek
,那peek
和map
有什么区别呢?
peek
的参数是Consumer
, 但map
的参数是Function
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
Function
也是一个FunctionalInterface
,这个接口需要实现下面的方法:
R apply(T t);
可以看到,这个方法是有返回值的,这跟Consumer
就不一样,所以我们一般用map
来修改元素的信息,而不是用peek
peek
方法接收一个Consumer
的入参. 了解λ表达式的应该明白 Consumer
的实现类应该只有一个方法,该方法返回类型为void. 它只是对Stream
中的元素进行某些操作,但是操作之后的数据并不返回到Stream
中,所以Stream
中的元素还是原来的元素.
map
方法接收一个Function
作为入参. Function
是有返回值的, 这就表示**map
对Stream
中的元素的操作结果都会返回到Stream
中去。**
可以通过这个示例来解释上面的说法:
public static void peekUnModified() {
Stream.of(1, 2, 3)
.peek(e -> e=e+1)
.forEach(e-> System.out.println("peek unModified: "+e));
}
public static void mapModified() {
Stream.of(1, 2, 3)
.map(e -> e=e+1)
.forEach(e->System.out.println("map modified: "+e));
}
可以自己理解一下,想想输出值是否和下面的结果一致,正确的输出结果为:
peek unModified: 1
peek unModified: 2
peek unModified: 3
map modified: 2
map modified: 3
map modified: 4
ps: 要注意一点的时,peek
虽然不会改变操作的对象(对象引用),但可以修改对象的属性,如开头我的那个例子中,修改了questionPageVO
的属性
总结
- 在使用
stream
中的peek
和map
时,要留意懒加载和是否被优化,都可能会导致方法不执行。 - 如果想要修改元素,则使用
map
。 peek
只用来查看元素时使用。
本文示例源码:gitee.com/fantasic/ja…
转载自:https://juejin.cn/post/7394376050372657204