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的基本定义和使用

  1. 先来看看peek的定义:
Stream<T> peek(Consumer<? super T> action);

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

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

void accept(T t);

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

  1. 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]
  1. 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

通过输出内容也可以看出,流式处理流程,是对应流中每一个元素,分别经历peekforEach操作(即一个元素执行完所有流程),而不是等peek完所有元素元素后再执行forEach

坑一:Stream的懒执行策略

之所以有流操作,是因为有时候处理的数据比较多,无法一次性加载到内存中。

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

什么是懒加载呢?

懒加载也叫intermediate operation, 在stream的方法中,大部分都是懒加载,另外部分则是terminal operation, 例如collectcount等,当有这种非懒加载的方法调用时,整个链式都会被执行,如开始的baseUse示例。

peekmap,都是懒加载方法,即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,那peekmap有什么区别呢?

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是有返回值的, 这就表示**mapStream中的元素的操作结果都会返回到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的属性

总结

  1. 在使用stream中的peekmap时,要留意懒加载和是否被优化,都可能会导致方法不执行。
  2. 如果想要修改元素,则使用map
  3. peek只用来查看元素时使用。

本文示例源码: https://gitee.com/fantasic/java-test-demo/tree/master/peek-map

  • 19
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值