ZetCode

Java Stream peek

最后修改于 2025 年 5 月 24 日

本文演示了如何使用 Java Stream peek 方法在处理过程中调试和检查流元素。

peek 方法是 Java Stream API 中的一个中间操作,它允许您在流的每个元素通过管道时对其执行非干扰性操作,并返回一个包含相同元素的新流。 它主要用于调试,接受一个 Consumer 函数来观察或记录元素,而无需更改它们。

例如,您可以使用 peek 来打印中间值或跟踪复杂流操作期间元素的状态。 虽然其主要目的是调试,但它也可以支持非干扰性副作用,例如日志记录或指标收集,前提是这些操作符合流 API 的非干扰性要求。 但是,开发人员应谨慎使用 peek,因为不当使用(例如,修改元素或引入有状态行为)可能会违反流语义并导致不可预测的结果,尤其是在并行流中。

基本 peek 语法

Java Stream API 中的 peek 方法定义了一个方法签名,该签名允许通过提供的操作检查流元素。 此签名非常简单,使其易于集成到流管道中以进行调试或非干扰性副作用。


Stream<T> peek(Consumer<? super T> action)

peek 方法接受一个 Consumer,这是一个函数式接口,它接受一个类型为 T(或超类型)的单个输入参数,并且不返回任何结果。 此 Consumer 定义要对每个流元素执行的操作,例如记录或检查其状态。 peek 操作是非破坏性的,返回一个包含与原始流相同元素的新流,确保管道的数据保持不变。

虽然主要用于调试(例如,打印中间值),但该操作必须是非干扰性的,以符合流 API 的要求,这意味着它不应修改流的源或引入有状态行为。 开发人员应谨慎行事,尤其是在并行流中,如果未正确管理,peek 的副作用可能会导致不可预测的结果。

调试流操作

在以下示例中,我们将使用 peek 来记录元素在通过流管道时的信息。 这对于调试和理解数据如何通过流非常有用。

Main.java
void main() {

    long count = Stream.of("apple", "banana", "cherry", "date")
        .peek(e -> System.out.println("Original: " + e))
        .filter(s -> s.length() > 4)
        .peek(e -> System.out.println("Filtered: " + e))
        .map(String::toUpperCase)
        .peek(e -> System.out.println("Mapped: " + e))
        .count();
    
    System.out.println("Total count: " + count);
}

此示例演示了如何使用 peek 来观察流处理不同阶段的元素。 每个 peek 调用都会记录元素的当前状态。

$ java Main.java
Original: apple
Original: banana
Filtered: banana
Mapped: BANANA
Original: cherry
Filtered: cherry
Mapped: CHERRY
Original: date
Total count: 2

理解惰性求值

peek 帮助可视化流的惰性求值行为。

Main.java
void main() {

    Stream.of("one", "two", "three", "four")
        .peek(e -> System.out.println("Before filter: " + e))
        .filter(e -> e.length() > 3)
        .peek(e -> System.out.println("After filter: " + e))
        .findFirst()
        .ifPresent(System.out::println);
}

此示例显示了流如何一次处理一个元素,直到满足终端操作。 在找到第一个匹配的元素后,findFirst 操作停止处理。

$ java Main.java
Before filter: one
Before filter: two
Before filter: three
After filter: three
three

跟踪状态变化

我们可以使用 peek 来观察可变对象中的状态变化。

Main.java
class Counter {

    int count = 0;
    
    void increment() { 
        count++; 
    }
    
    @Override 
    public String toString() { 
        return "Count: " + count; 
    }
}

void main() {

    List<Counter> counters = new ArrayList<>();
    
    for (int i = 0; i < 5; i++) {
        counters.add(new Counter());
    };
    
    counters.stream()
        .peek(c -> System.out.println("Before: " + c))
        .forEach(Counter::increment);
    
    System.out.println("\nAfter processing:");
    counters.forEach(System.out::println);
}

此示例显示了 peek 如何观察可变对象中的状态变化。 请注意,通常不鼓励在 peek 中修改对象,因为它可能导致意外行为。

$ java Main.java
Before: Count: 0
Before: Count: 0
Before: Count: 0
Before: Count: 0
Before: Count: 0

After processing:
Count: 1
Count: 1
Count: 1
Count: 1
Count: 1

记录转换

记录复杂管道中的中间转换。

Main.java
record Product(String name, double price, int stock) {
}

void main() {

    Stream.of(
            new Product("Laptop", 999.99, 5),
            new Product("Phone", 699.99, 10),
            new Product("Tablet", 349.99, 0),
            new Product("Monitor", 249.99, 8)
        )
        .peek(p -> System.out.println("Original: " + p))
        .filter(p -> p.stock() > 0)
        .peek(p -> System.out.println("In stock: " + p))
        .map(p -> new Product(p.name(), p.price() * 0.9, p.stock())) // 10% discount
        .peek(p -> System.out.println("Discounted: " + p))
        .forEach(p -> System.out.println("Final: " + p));
}

此示例显示了 peek 如何帮助理解涉及过滤和映射的更复杂的流管道中的每个转换步骤。

来源

Java Stream peek 文档

在本文中,我们探讨了 Java Stream peek 方法。 虽然主要用于调试,但它可以用于观察流处理而无需修改流内容。 请记住,peek 通常不应用于修改状态或影响流结果的操作。

作者

我叫 Jan Bodnar,我是一位充满热情的程序员,拥有丰富的编程经验。 自 2007 年以来,我一直在撰写编程文章。 到目前为止,我已经撰写了 1,400 多篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出所有Java教程