简体   繁体   中英

Non-terminal forEach() in a stream?

Sometimes when processing a Java stream() I find myself in need of a non-terminal forEach() to be used to trigger a side effect but without terminating processing.

I suspect I could do this with something like.map(item -> f(item)) where the method f performs the side effect and returns the item to the stream, but it seems a tad hokey.

Is there a standard way of handling this?

Yes there is. It is called peek() (example from the JavaDoc ):

Stream.of("one", "two", "three", "four")
     .peek(e -> System.out.println("Original value: " + e))
     .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());

No, there is not.

peek() will only operate on all elements when forced to by a following operation. Can you predict what will be printed by this code?

public class Test
{
    private static final AtomicBoolean FLAG = new AtomicBoolean(false);

    private static void setFlagIfGreaterThanZero(int val)
    {
        if (val > 0) {
            FLAG.set(true);
        }
    }

    public static void main(String[] args)
    {
        FLAG.set(false);

        // Test 1
        IntStream.range(0, 10)
                 .peek(Test::setFlagIfGreaterThanZero)
                 .findFirst();

        System.out.println(FLAG.get());
        FLAG.set(false);

        // Test 2
        IntStream.range(0, 10)
                 .peek(Test::setFlagIfGreaterThanZero)
                 .sorted()
                 .findFirst();

        System.out.println(FLAG.get());
        FLAG.set(false);

        // Test 3
        IntStream.range(0, 10)
                 .boxed()
                 .peek(Test::setFlagIfGreaterThanZero)
                 .sorted()
                 .findFirst();

        System.out.println(FLAG.get());
        FLAG.set(false);

        // Test 4
        IntStream.range(0, 10)
                 .peek(Test::setFlagIfGreaterThanZero)
                 .filter(x -> x == 0)
                 .toArray();

        System.out.println(FLAG.get());
    }
}

The answer is:

false
false
true
true

That output might be intuitive if you have a solid understanding of Java Streams, but hopefully it also indicates that it's a very bad idea to rely on peek() as a mid-stream forEach() .

map() also suffers the same issue. As far as I'm aware, there is no Stream operation that guarantees a sort of "process every element without taking shortcuts" behavior in every case independent of the prior and following operations.

Although this can be a pain, the short-circuiting behavior of Streams is an important feature. You might find this excellent answer to be useful: https://stackoverflow.com/a/32194320/507761

One option is to use map() with fluent APIs. The second map here uses the class's fluent API to mutate the object and then map it onto itself. The caveats mentioned elsewhere still apply, of course, but for an application like this, in which you're gathering and modifying a subset of the elements of the stream, this can be a good solution.

return data.values().stream()
            .filter(Project.class::isInstance)
            .map(Project.class::cast)
            .map(p -> p.children(Collections.emptyList()))
            .collect(Collectors.toList());

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM