1. 简介
Java Stream API 为我们提供了一种强大的方式来处理数据集合。它支持链式调用、延迟执行等特性,极大提升了代码的可读性和性能。
在本篇文章中,我们将聚焦于一个经常被误解的方法:peek()
。
2. 快速上手示例
先来看一个简单的例子。假设我们有一个字符串流,想要将其中每个名字打印到控制台。
由于 peek()
接收一个 Consumer<T>
类型的参数,看起来非常适合用来做这件事,于是我们尝试如下写法:
Stream<String> nameStream = Stream.of("Alice", "Bob", "Chuck");
nameStream.peek(System.out::println);
✅ 然而,这段代码并不会有任何输出!
为什么会这样?我们需要回顾一下 Stream 的生命周期机制才能理解。
3. 中间操作 vs 终止操作
Stream 的处理流程可以分为三个部分:
- 数据源(source)
- 零个或多个中间操作(intermediate operations)
- 零个或一个终止操作(terminal operation)
其中,中间操作是惰性求值的,也就是说,在没有触发终止操作之前,任何中间操作都不会真正执行。
⚠️ 这就是为什么上面那段代码没有输出的原因 —— 因为没有调用终止操作!
4. peek() 的正确使用姿势
回到最初的例子,既然 peek()
是中间操作,那我们必须搭配一个终止操作来“激活”整个流水线。比如我们可以加上 .forEach(...)
:
Stream<String> nameStream = Stream.of("Alice", "Bob", "Chuck");
nameStream.peek(System.out::println)
.forEach(System.out::println); // 加上终止操作
不过如果你只是想遍历并打印元素,直接用 forEach()
更加简单粗暴:
Stream<String> nameStream = Stream.of("Alice", "Bob", "Chuck");
nameStream.forEach(System.out::println);
peek() 的主要用途:调试和状态修改
根据 Javadoc 描述:
“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()
最常见的用途是调试,用于观察流经管道某个阶段的数据。
举个官方文档中的例子:
Stream.of("one", "two", "three", "four")
.filter(e -> e.length() > 3)
.peek(e -> System.out.println("Filtered value: " + e))
.map(String::toUpperCase)
.peek(e -> System.out.println("Mapped value: " + e))
.collect(Collectors.toList());
在这个例子中,peek()
可以帮助我们看到每一步操作的结果,非常适合排查问题。
peek() 还能做什么?
除了调试,peek()
还有一个常见场景是修改对象内部状态,而不需要替换整个对象。
例如,我们要把所有用户的姓名转成小写后再打印:
Stream<User> userStream = Stream.of(new User("Alice"), new User("Bob"), new User("Chuck"));
userStream.peek(u -> u.setName(u.getName().toLowerCase()))
.forEach(System.out::println);
如果用 map()
实现,虽然也能做到,但语义上就不如 peek()
清晰 —— 毕竟我们并不想改变元素本身的身份,只是想对其做一些副作用操作。
5. 小结
在这篇短文中,我们回顾了 Stream 的生命周期机制,并重点介绍了 peek()
方法的使用方式。
总结几个关键点:
✅ peek()
是中间操作,必须配合终止操作才会生效
✅ 主要用于调试,观察中间结果
✅ 在需要对元素做副作用操作(如修改状态)时也非常实用
❌ 不要把它当成 forEach()
来使用,除非你知道自己在做什么
一如既往,文中示例代码可以在 GitHub 仓库 中找到。