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.
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?
Reveal 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.
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
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.
- 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.
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?
Reveal 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.
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
- 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
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?
- 1Read left to right
Values move through the pipeline in the order the calls are written, so the filter is reached before collect.
- 2Identify the filter stage
The filter keeps only names that start with Fred. Ann and Bo fail this test and never reach the collect step.
- 3Identify the collect stage
The collect step with
Collectors.toList()gathers whatever survived the filter into a new List. - 4Trace an unexpected value
If freds contained an unexpected string, check the filter condition first, because collect only gathers what the filter let through.
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 likeUsing subtraction in reduce over 10, 5, and 2 and expecting the same answer regardless of grouping.
Why it failsThe source says the binary operator should be associative. Subtraction is not, so the grouping changes the result.
Do this insteadConfirm 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 likeExpecting collect to return a single total number.
Why it failsReduce returns one value computed by combining the stream's values. Collect returns a new Collection holding the elements that reached it.
Do this insteadAsk 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 likeInspecting a List from collect and not checking the filter that fed it.
Why it failsCollect only gathers what the filter let through, so an unexpected element usually originates upstream.
Do this insteadTrace 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
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.