33

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?

Ian
  • 1,507
  • 3
  • 21
  • 36
  • peek should not trigger any side effect. Do the forEach before or after your stream operation. If you think you need the forEach during your stream, the stream operation is most probably to complexe. – Herr Derb Dec 24 '20 at 08:12

3 Answers3

26

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());
Thomas Kläger
  • 17,754
  • 3
  • 23
  • 34
  • 8
    Although peek alone won't do anything without a terminal method. – Bohemian Nov 16 '16 at 06:07
  • 4
    @Thomas: Thanks. Peek, what an obvious name. How on earth did I miss that...! :) – Ian Nov 16 '16 at 06:22
  • @Bohemian: Doesn't that apply to most of the stages? Isn't pretty much everything lazy? – Ian Nov 16 '16 at 06:23
  • 2
    @Ian yes, but your question specifically asked for a "non terminal forEach". I just wanted to emphasise that isn't anything like forEach. Actually, using peek to modify items is officially frowned upon. It is "OK" to use for non-mutating operations, but it was intended for cross cutting concerns like logging. – Bohemian Nov 16 '16 at 06:29
  • @Bohemian: Fair point, that might have been ambiguous. I did mean, as Thomas recognised, that I wanted an equivalent to forEach() that I could use in a non-terminal position with a different terminal clause. Why exactly is using peek() for mutating operations frowned upon? Something to do with parallel operations? – Ian Nov 16 '16 at 06:34
  • @Ian: Quote from the docs: "This method exists mainly to support debugging". As with other debug code, it should not influence other behavior. Related (but somewhat unsatisfying question on software engineering: http://softwareengineering.stackexchange.com/q/308977/187318) – Hulk Nov 16 '16 at 11:24
  • 1
    But more generally: Don't mutate elements in streams in general, see e.g. http://stackoverflow.com/q/33764216/2513200, especially the comment by Brian Goetz: [This usage is a clear abuse of map(). Do your mutation inside the forEach().](http://stackoverflow.com/questions/33764216/mutate-elements-in-a-stream#comment55309424_33764216) (it doesn't get any more authoritative than that). – Hulk Nov 16 '16 at 11:30
  • @Hulk: Many thanks for those links. On the whole I'm less than impressed by author's opinions of what their methods exist mainly for, but the comment by Goetz does give me significant pause. Regrettably though we're still forced to try to read between the lines to understand /why/. I'm guessing, in this case, that the problem is that the OP might be expecting an operation that mutates /all/ stream elements, rather than one which operates only of those selected by the terminator? This latter behaviour is standard practice in dataflow processing though ... cont – Ian Nov 16 '16 at 13:55
  • (cont from above)... and I've never had it advise against modifying data in my pipeline until now. Have I missed something peculiar about Java streams? – Ian Nov 16 '16 at 13:56
  • One thing I can think of (that is not special to Streams, but to mutation during iteration in general) is that it might invalidate the source container if the contained objects are poorly designed (e.g. `hashCode`/`equals` depending on mutable state or a `Tree` sorted based on a mutable field...). But such objects should not be exposed in the first place. – Hulk Nov 16 '16 at 14:24
  • 7
    [this answer](http://stackoverflow.com/a/33636377/2711488) provides examples of how `peek` can break or not work as intended, when being used for non-debugging purposes. – Holger Nov 16 '16 at 16:20
  • 5
    If you need more than one `Consumer` for your elements you could also chain your `Consumer`s with `c1.andThen(c2)` (assuming that `c1` and `c2` are `Consumer`s) – Thomas Kläger Nov 16 '16 at 16:38
  • @Holger: That's a great answer and I read that earlier. But, I think, isn't that answer applicable to /all/ dataflow programming? It's not, I think, an argument for not using peek to call methods on items that are being processed. For instance I've just coded map.get(key).stream().peek(filedata -> filedata.verifyKey(key)).map(filedata -> filedata.getFullPath()).collect(Collectors.toList()) to invoke an integrity check on items that I'm collecting. This is clearly not debugging but not, afaik, dangerous. – Ian Nov 17 '16 at 01:17
  • @Hulk: Agreed. This is not an issue with streams, per se. – Ian Nov 17 '16 at 01:19
  • 1
    @ThomasKläger: Thanks for the update. That's nice too. And avoids nervousness around peek(). – Ian Nov 17 '16 at 01:20
  • 1
    @Ian: `peek` is the only intermediate operation intended for side effects. Hence, the limitations for these side effects *are* special. What does your `verifyKey` method do when the integrity check fails? – Holger Nov 17 '16 at 08:01
  • @Thomas Kläger: `c2` could also be a lambda expression or method reference. – Holger Nov 17 '16 at 08:03
  • @Holger: Aborts processing by throwing an exception. – Ian Nov 17 '16 at 09:07
  • Then it’s not so far away from a debugging statement. – Holger Nov 17 '16 at 09:11
14

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) {
        // Test 1
        FLAG.set(false);
        IntStream.range(0, 10)
                 .peek(Test::setFlagIfGreaterThanZero)
                 .findFirst();
        System.out.println(FLAG.get());

        // Test 2
        FLAG.set(false);
        IntStream.range(0, 10)
                 .peek(Test::setFlagIfGreaterThanZero)
                 .sorted()
                 .findFirst();
        System.out.println(FLAG.get());

        // Test 3
        FLAG.set(false);
        IntStream.range(0, 10)
                 .peek(Test::setFlagIfGreaterThanZero)
                 .filter(x -> x == 0)
                 .toArray();
        System.out.println(FLAG.get());

        // Test 4
        FLAG.set(false);
        IntStream.range(0, 10)
                 .boxed()
                 .peek(Test::setFlagIfGreaterThanZero)
                 .sorted()
                 .findFirst();
        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 another question on this topic to be useful.

Matthew Read
  • 1,365
  • 1
  • 30
  • 50
-1

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());
Ernest Friedman-Hill
  • 80,601
  • 10
  • 150
  • 186