peek() 是中间操作,不触发流执行,必须接 collect()、forEach() 等终止操作;其日志输出内容取决于在链中的位置;打印自定义对象需避免 null 和哈希码;并行流中执行顺序不保证。

peek() 不执行?检查有没有终止操作
写了 peek() 却没输出日志,大概率是因为流没真正跑起来。它只是中间操作,不触发执行——必须接上 collect()、forEach()、count() 等终止操作,整条流水线才会激活。
常见错误写法:
list.stream().peek(System.out::println).filter(x -> x > 10); // ❌ 什么都不会打印
正确做法是补上终止操作:
.collect(Collectors.toList()).forEach(System.out::println)-
.count()(适合只关心数量、不关心内容的埋点)
在 filter/map 之间插 peek,位置决定你看到什么
peek() 的作用点就是它在链中出现的位置:它看到的是“刚经过前一个操作、还没进下一个操作”的元素状态。位置错了,日志就失去调试意义。
比如这段代码:
List<String> result = Arrays.asList("a", "java", "stream")
.stream()
.peek(s -> System.out.println("→ raw: " + s)) // 打印全部三个
.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());
关键点:
- 放在
filter()前:看到原始输入,适合确认数据源是否符合预期 - 放在
filter()后:看到存活元素,适合验证过滤逻辑是否漏/误杀 - 放在
map()后:看到转换结果,适合检查格式、空值、编码等副作用
记录自定义对象时别直接 System.out::println
如果流里是 User、Order 这类对象,用 peek(System.out::println) 很可能只输出 User@4f9a339b 这种哈希码,看不出实际字段。
推荐写法(显式控制输出内容):
.peek(u -> System.out.printf("user[id=%d, name=%s, status=%s]%n",
u.getId(),
u.getName() != null ? u.getName() : "null",
u.getStatus()))
注意:u.getName() 如果可能为 null,不判空会导致 peek 内部抛 NullPointerException,整个流中断——这不是日志问题,而是流程崩溃。
并行流中 peek 的执行顺序不保证
用 parallelStream() 时,peek() 的调用顺序和线程调度强相关,输出日志可能乱序。比如你期望按输入顺序看到 “id=1 → id=2 → id=3”,实际可能是 “id=2 → id=1 → id=3”。
这不是 bug,是设计使然:peek() 的 Javadoc 明确说明 “不保证执行顺序”,尤其在并行场景下。如果你依赖日志顺序做分析(比如排查某次请求的完整路径),要么改用单线程流,要么在日志里加上 Thread.currentThread().getId() 或时间戳辅助对齐。
真正容易被忽略的是:哪怕加了时间戳,并行流中多个 peek 调用也可能因 CPU 调度微小差异而交错,导致同一请求的日志散落在不同时间点——这时靠日志本身已不足以还原上下文,得配合 traceId 或外部监控系统。


















