Concepts / Terminal Operations in Stream Processing

Terminal Operations in Stream Processing

reduce combines the values of a stream into one result using a BinaryOperator, and a sum is one example where the operation is addition.

  • Programming

Why Pipelines End With a Terminal Step

A stream pipeline usually ends with a terminal operation. Its job is to turn a whole stream of values into an answer. Asking for the total of a list of numbers or the largest of a set of values are common examples of this kind of question. Writing a separate terminal operation for every possible question would be impractical, so the Stream API offers a more general tool called reduce, which can express many of these computations.

A second tool, collect, answers a different kind of question. Instead of producing one value, it gathers the elements of the stream into a new collection. Both tools end a pipeline. This article explains how each one works and how to tell them apart by looking at what each one returns.

Watching reduce Fold Values Together

Think of reduce as a rule for combining two values at a time. You give the stream a rule that takes two inputs and returns one output. The stream applies that rule repeatedly, merging values until only one result is left. The best way to see this is to follow the running value step by step. Before you read the diagram, try to predict what the running value will be after each element is combined.

What do you think happens?

A stream holds the values 3, 4, and 5, and the rule is addition. What is the running value after the element 4 has been combined?

  • 4

  • 7

  • 12

Reveal answer
Answer:

7

The running value starts with the first element, 3. Combining it with 4 under addition gives 7. Combining 7 with 5 then gives the final result, 12.

3start with first element7after combining 412final result after 5add 4add 5
How does the accumulated value change at each step as the operator combines the next element, until one final result remains?

Each arrow in the diagram is one application of the combining rule. The value on the left of each arrow is the accumulated value so far, and the new element is the input from the stream. The final node is the only value that remains once every element has been merged. This is why reduce is said to produce one result from many values.

The BinaryOperator Rule

Definition

The rule you pass to reduce is a BinaryOperator. It takes two values of the same kind and returns one value of that kind. A reduce operation combines the values from a stream using a BinaryOperator, and a sum is one case where the binary operation is addition. When the rule is multiplication, the final result is a product.

The source points out a useful gap in the built-in terminal operations. The Stream API has no ready-made terminal operation that computes the product of the values in a stream. You can still get that result, because reduce lets you supply the combining rule yourself. The source's own illustration is the product of all the non-zero elements in an array of double values. That case shows a custom rule filling a missing built-in.

java
Notes on the code
  • The lambda (a, b) -> a + b is a BinaryOperator that adds two values
  • The identity 0 is the starting value used when summing
  • Swapping the lambda body for a * b gives a product using a different identity, 1

Why Grouping Can Change the Answer

The rule you give reduce must be a BinaryOperator, and the source adds a condition on it: the binary operator should be associative. An operator is associative when the way its applications are grouped does not change the result. The source states the consequence directly: the order in which the operator is applied does not matter. Addition is the standard example, since adding three numbers gives the same total whether you combine the first two or the last two first.

left grouping(10 - 5) - 210 - 5gives 510first value2last valueright grouping10 - (5 - 2)5 - 2gives 3combinescombinescombinescombines
Why does grouping (a+b)+c versus a+(b+c) give the same result for addition but can give different results for a non-associative operator like subtraction?

The diagram shows the same values 10, 5, and 2 with two different groupings. Grouping the first two values first gives 5, and then subtracting 2 gives 3. Grouping the last two values first gives 3, and then subtracting that from 10 gives 7. Subtraction is not associative, so the grouping changes the answer. Addition does not have this problem, which is why a sum is safe to compute with reduce regardless of how the stream is split up.

What do you think happens?

Using the values 10, 5, and 2 with subtraction, do the left grouping and the right grouping give the same result?

  • Yes, both give 3

  • No, they give 3 and 7

  • Yes, both give 7

Reveal answer
Answer:

No, they give 3 and 7

Left grouping computes (10 - 5) - 2, which is 3. Right grouping computes 10 - (5 - 2), which is 7. Because subtraction is not associative, the answer depends on grouping.

How collect Builds a New Collection

The collect operation has a different purpose. The source describes it as accumulating the input elements into a new Collection. Where reduce returns a single combined value, collect returns a collection that holds the elements that reached it. The usual form names the collector as Collectors.toList(), so the elements are gathered into a List.

stream elementsvalues produced upstreamfilter stagepasses some elementscollectCollectors.toList()new Listholds collected elementselements flowfiltered elementsstored once
How do the elements flowing out of the stream end up stored in a new collection object, and what is the boundary between the stream and the collection?

The boundary sits at the collect step. Upstream of it, values are still flowing through the stream one at a time. At the collect step, the elements that reached it are accumulated into a new collection object, and that object is what the program keeps after the pipeline finishes. The stream itself is consumed by this process, and the List is the durable result.

Tracing a Result Back Through the Pipeline

java
Notes on the code
  • filter is an intermediate step that passes along only matching elements
  • collect is the terminal step that builds the final List
  • The name freds refers to the collected result, not to the stream
Worked example

Finding which stage produced the final List

Given the code above, the program prints freds and shows Fred and Frederick. Which stage decided that Ann and Bo are absent, and which stage built the List?

  1. 1
    Read left to right

    Values move through the pipeline in the order the calls are written, so the filter is reached before collect.

  2. 2
    Identify the filter stage

    The filter keeps only names that start with Fred. Ann and Bo fail this test and never reach the collect step.

  3. 3
    Identify the collect stage

    The collect step with Collectors.toList() gathers whatever survived the filter into a new List.

  4. 4
    Trace an unexpected value

    If freds contained an unexpected string, check the filter condition first, because collect only gathers what the filter let through.

Result

The filter decided which names survived, and the collect step stored them in the List named freds.

This is the practical skill the source points toward. When a final answer is wrong, the cause is usually one of the stages that produced it. For a collect result, the filter is the first place to look. For a reduce result, check both the stage that selects values and the reduce stage that combines them. Tracing each stage's input and output shows which one diverged from what you expected.

Mistakes Learners Make With reduce and collect

  • Common mistake

    Assuming the operator order does not matter for any operator

    What it looks like

    Using subtraction in reduce over 10, 5, and 2 and expecting the same answer regardless of grouping.

    Why it fails

    The source says the binary operator should be associative. Subtraction is not, so the grouping changes the result.

    Do this instead

    Confirm associativity before relying on reduce, and use addition or multiplication when the order of application must not matter.

  • Common mistake

    Confusing reduce with collect because both end a pipeline

    What it looks like

    Expecting collect to return a single total number.

    Why it fails

    Reduce returns one value computed by combining the stream's values. Collect returns a new Collection holding the elements that reached it.

    Do this instead

    Ask what the terminal step returns. One value means reduce, and a collection of elements means collect.

  • Common mistake

    Looking only at the terminal step when the result is wrong

    What it looks like

    Inspecting a List from collect and not checking the filter that fed it.

    Why it fails

    Collect only gathers what the filter let through, so an unexpected element usually originates upstream.

    Do this instead

    Trace the filter condition and the input and output of each stage to find where the value diverged.

  • Common mistake

    Forgetting that the Stream API has no built-in product operation and trying to find one

  • Common mistake

    Choosing a starting value for reduce that does not match the operator

Predicting Which Stage Produced a Result

MEDIUM

A pipeline filters a stream of numbers to keep only those greater than 2, then calls reduce with addition and starting value 0. The input stream holds 1, 3, 4, and 6. Before you compute, predict which values reach the reduce stage, and then calculate the final result. Finally, explain which stage you would inspect first if the answer were 13 instead of 13 minus a stray value.

Hints
  • Apply the filter first and list only the values that pass the test greater than 2
  • Reduce with addition and starting value 0 sums whatever reached it
  • If the answer is wrong, decide whether the filter or the reduce stage diverged from your expectation

Summary of reduce and collect

Both reduce and collect end a stream pipeline, and the difference is what they return. Reduce combines the stream's values with a BinaryOperator until one result remains. Collect accumulates the elements into a new Collection, such as a List built with Collectors.toList().

At a glance

  • reduce combines the values of a stream into one result by repeatedly applying a BinaryOperator, and a sum is the case where the operator is addition.
  • The binary operator should be associative, because a non-associative operator such as subtraction can give different answers depending on how operations are grouped.
  • reduce can compute a product even though the Stream API has no built-in terminal operation for it, by supplying a custom combining rule.
  • collect accumulates the input elements into a new Collection, such as a List built with Collectors.toList(), instead of returning one value.
  • When a final result is wrong, trace each pipeline stage's input and output, starting with the filter, to find which stage diverged.