peek是中间操作,必须配合终止操作才执行;其位置决定观察的元素状态,仅用于调试,不可用于副作用操作。

Stream.peek必须配合终止操作才生效
单独写 stream().peek(...) 不会打印任何东西,因为 peek 是中间操作,流不会自动执行——它只在遇到 collect、forEach、count 等终止操作时才真正触发整条流水线。
常见错误是写了 peek 却没加终止操作,然后纳闷“为什么没输出”。比如下面这段代码什么都不会打印:
List<string> list = Arrays.asList("a", "bb", "ccc");
list.stream().peek(s -> System.out.println("seen: " + s)).filter(s -> s.length() > 1); // ❌ 缺少终止操作</string>
补上 .collect(Collectors.toList()) 或 .count() 就能立刻看到输出。
在 filter/map 等中间步骤之间插入 peek 的位置很关键
peek 的作用点是“当前流中已存在、且尚未被后续操作处理”的元素。它不是在操作执行前或后“钩住”,而是紧贴在它所在的位置对每个流经的元素执行一次。
-
filter前的peek:看到原始输入元素 -
filter后的peek:只看到通过过滤的元素(数量可能变少) -
map后的peek:看到的是转换后的新值,不是原值
例如:
List<string> result = Arrays.asList("a", "java", "stream")
.stream()
.peek(s -> System.out.println("before filter: " + s)) // 打印 a, java, stream
.filter(s -> s.length() > 2)
.peek(s -> System.out.println("after filter: " + s)) // 只打印 java, stream
.map(String::toUpperCase)
.peek(s -> System.out.println("after map: " + s)) // 打印 JAVA, STREAM
.collect(Collectors.toList());</string>
用 peek 调试自定义对象时别直接打印对象引用
如果流里是 User、Order 这类对象,peek(user -> System.out.println(user)) 很可能只输出类似 User@3d4eac69,看不出实际字段值。
应该显式选择关键字段,或者确保 toString() 已重写:
- 推荐写法:
.peek(u -> System.out.printf("id=%d, name=%s, active=%b%n", u.getId(), u.getName(), u.isActive())) - 避免写法:
.peek(System.out::println)(除非你确认 toString 已覆盖) - 注意:如果字段可能为
null,记得加判空,否则peek内部抛异常会导致整个流中断
peek 不适合做业务逻辑或副作用操作
peek 的设计定位就是“调试观察”,JVM 不保证它一定会执行(比如短路操作 findFirst() 可能只处理前几个元素),也不保证执行顺序在并行流中严格一致。
所以这些都属于危险用法:
- 在
peek里修改外部变量(如counter++)——结果不可靠 - 用
peek发 HTTP 请求或写文件——可能漏调、重复调、或并发错乱 - 依赖
peek的执行次数做判断——并行流下不成立
真要执行副作用,请用 forEach(但注意它不能链式继续)或把逻辑移到 map/filter 条件中。
最常被忽略的一点:peek 输出混在控制台日志里,一旦流数据量大或并发执行,不同线程的打印会交错,看不出哪个 peek 属于哪次处理。调试复杂流时,建议加上下文标识,比如 "[step1-filter]" 前缀,否则很快就会分不清哪行是谁打的。











