Filtering and Mapping Streams
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 in One Answer
A stream pipeline usually ends with a terminal operation, and many terminal operations exist to turn a whole stream of values into one answer. Asking for the total of a list of numbers or the largest of a set of values are common examples. Rather than writing a separate terminal operation for every possible question, the Stream API offers a more general tool called reduce, which can express many of these computations.
The same idea also explains a second tool, collect. It 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, why the rule given to reduce has to satisfy a special property, and how to trace a result back to the stage that produced it.
Reduce as a Pairwise Combining Rule
Think of reduce as a rule for combining two values at a time. You hand the stream a rule that takes two inputs and returns one output. The stream then uses that rule repeatedly, merging values until only a single result is left. When the rule is addition, the final result is a sum. When the rule is multiplication, the final result is a product. In general terms, a reduce operation combines the values from a stream using a BinaryOperator, and a sum is one case where the binary operation is addition.
A BinaryOperator is a rule that takes two inputs of the same type and returns one output of that type. Passing a BinaryOperator to reduce tells the stream how to merge two of its values into one.
The easiest way to see what reduce does is to watch the accumulated value change. The code below sums five numbers. Notice that the program never writes a loop; the stream applies the combining rule for you.
14- Integer::sum is the combining rule: it takes two integers and returns their sum
- The first argument 0 is the starting value that the first element is combined with
- reduce returns one value, so total is a single int rather than a collection
Read the diagram from left to right. Each box is the accumulated value after one step. Each step takes the previous accumulated value and one new element from the stream, and combines them with the rule. The final box, 14, is the single result that reduce returns.
The Stream API has no ready-made terminal operation for the product of the values in a stream. You can still compute it, 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. The code below follows that pattern: a filter removes the zeros, and reduce multiplies what remains. The starting value 1.0 is the value that leaves any number unchanged when multiplied, so it works as the starting point for a product.
24.0- The filter stage removes the 0.0 so it cannot wipe out the product
- The lambda (a, b) -> a * b is the BinaryOperator that multiplies two values
- The product 2.0 times 3.0 times 4.0 is 24.0
Associativity and the Order of Combining
The rule you give reduce must be a BinaryOperator, and the source adds a condition: 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 practical consequence directly: the order in which the operator is applied does not matter. Because of this, you should check the property before relying on reduce.
What do you think happens?
The values 8, 4, 2, 1 are combined with subtraction. Does pairing (8 - 4) with (2 - 1) give the same answer as combining them strictly left to right?
Reveal answer
No, the answers differ. Left to right gives 1, while the balanced pairing gives 3.
Left to right: ((8 - 4) - 2) - 1 = 4 - 2 - 1 = 1. Balanced pairing: (8 - 4) - (2 - 1) = 4 - 1 = 3. Subtraction is not associative, so the grouping changes the answer.
The two cases below show how to tell when grouping matters. The first case is one where it does not, and the second is one where it does.
How collect Gathers Elements
The collect operation has a different purpose from reduce. 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.
[Fred, Fredrick]- The filter is an intermediate step: it passes along only the names that start with Fred
- collect is the terminal step that builds the new List from the elements that got through
- freds holds the collected result, not the stream itself
Read this example from left to right, since that matches the order in which values move through the pipeline. collect only gathers what the filter let through. If freds ever contains a value you did not expect, the filter condition is the first place to check.
Tracing Which Stage Produced a Result
When a pipeline gives a wrong answer, the cause is usually one of its stages. The filter stage decides which values reach the reduction, and the reduce stage decides how they are combined. Tracing each stage's input and output shows which one diverged from what you expected. The diagram below shows a pipeline with a filter, a map, and a reduce in sequence.
Finding which stage produced the final value
A stream holds the values 1 through 6. The pipeline keeps only the even values, multiplies each remaining value by 10, and then reduces the results with addition. What is the final value, and which stage produced each part of it?
- 1Filter stage
The filter keeps the even values 2, 4, and 6. It drops 1, 3, and 5. Only the filter removes elements.
- 2Map stage
The map transforms each surviving element. It turns 2, 4, and 6 into 20, 40, and 60. The map changes values but does not remove any.
- 3Reduce stage
The reduce combines 20, 40, and 60 with addition. Starting from 20, adding 40 gives 60, and adding 60 gives 120.
The final value is 120. The filter decided which values reached the map, the map transformed them, and the reduce produced the single result.
Mistakes Beginners Make with reduce and collect
Common mistake Assuming reduce always gives the same answer regardless of how it groups the values
What it looks likeCombining 8, 4, 2, 1 with subtraction left to right gives 1, but a balanced pairing gives 3.
Why it failsSubtraction is not associative, so changing the grouping changes the result.
Do this insteadConfirm that the binary operator is associative before relying on reduce.
Common mistake Thinking collect returns a single value
What it looks likeUsing collect(Collectors.toList()) and expecting one number or string back.
Why it failscollect accumulates the elements into a new Collection, so the result is a collection.
Do this insteadUse reduce when you want one combined value, and collect when you want a collection of the elements.
Common mistake Blaming the reduce stage for a result that the filter caused
What it looks likeA freds list contains an unexpected name, and the programmer rewrites the reduction logic.
Why it failscollect only gathers what the filter let through, so the filter condition may be the source of the extra element.
Do this insteadTrace the input and output of each stage, starting with the filter.
Common mistake Forgetting that a terminal operation ends the pipeline and that its result is what the caller receives
Practice Tracing a Reduce
A stream holds the values 8, 4, 2, 1. Compute the result of reducing them with subtraction once strictly left to right, and once as a balanced pairing that combines (8 - 4) and (2 - 1) first. Then identify which step in the balanced version is the one that makes it differ from the left-to-right version.
Hints
- Write out each pair of parentheses explicitly before calculating
- Compare the expression ((8 - 4) - 2) - 1 with (8 - 4) - (2 - 1)
- The difference comes from how 2 and 1 are combined, not from the values 8 and 4
Key Takeaways
- reduce combines the values of a stream into one result by repeatedly applying a BinaryOperator, and a sum is one case where the operation is addition.
- The binary operator should be associative, meaning its grouping does not change the result. Addition is associative, while subtraction is not, so the same values can produce different answers under different groupings.
- reduce can compute a product by supplying a combining rule that the Stream API does not provide as a built-in terminal operation.
- collect accumulates the elements that reach it into a new Collection, such as a List built with
Collectors.toList(), while reduce returns a single value. - To find where a result came from, trace each stage of the pipeline: the filter decides which values arrive, and the reduce or collect decides what is produced from them.