Concepts / Filtering and Mapping Streams

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.

  • Programming

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.

Definition

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.

java
Output
14
Notes on the code
  • 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
0starting value30 + 343 + 184 + 498 + 1149 + 5combine with 3combine with 1combine with 4combine with 1combine with 5
After each step of this reduce, what is the accumulated value, and which two values were combined to produce it?

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.

java
Output
24.0
Notes on the code
  • 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?

  • Yes, the answer is the same

  • No, the answers differ

Reveal answer
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.

java
Output
[Fred, Fredrick]
Notes on the code
  • 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.

1 to 6six values enterFilter keep evendrops 1, 3, 5Map times 102, 4, 6 become 20, 40, 60Reduce sumfinal result 120elements2, 4, 620, 40, 60
Given a filter, a map, and a reduce in sequence, which stage transforms each element, which stage drops elements, and where does the final value come from?
Worked example

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?

  1. 1
    Filter stage

    The filter keeps the even values 2, 4, and 6. It drops 1, 3, and 5. Only the filter removes elements.

  2. 2
    Map 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.

  3. 3
    Reduce stage

    The reduce combines 20, 40, and 60 with addition. Starting from 20, adding 40 gives 60, and adding 60 gives 120.

Result

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 like

    Combining 8, 4, 2, 1 with subtraction left to right gives 1, but a balanced pairing gives 3.

    Why it fails

    Subtraction is not associative, so changing the grouping changes the result.

    Do this instead

    Confirm that the binary operator is associative before relying on reduce.

  • Common mistake

    Thinking collect returns a single value

    What it looks like

    Using collect(Collectors.toList()) and expecting one number or string back.

    Why it fails

    collect accumulates the elements into a new Collection, so the result is a collection.

    Do this instead

    Use 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 like

    A freds list contains an unexpected name, and the programmer rewrites the reduction logic.

    Why it fails

    collect only gathers what the filter let through, so the filter condition may be the source of the extra element.

    Do this instead

    Trace 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

MEDIUM

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.