Java Tutorial
🔍

Java Stream reduce() Method

Java Stream reduce() Method

reduce() is the terminal operation that folds every element of a stream into a single result, applying a combining function repeatedly — first to the initial two values, then to that result and the next element, and so on until only one value is left. It is the general-purpose aggregation mechanism sum(), max(), and min() are all effectively built on top of, and it is the operation to reach for the moment an aggregation is needed that none of those dedicated methods already cover.

What Is reduce()?

reduce() comes in three overloaded forms. Optional<T> reduce(BinaryOperator<T> accumulator) combines elements with no starting value, returning Optional<T> because an empty stream would have nothing to give back. T reduce(T identity, BinaryOperator<T> accumulator) takes a starting value, called the identity, which doubles as the result for an empty stream and always guarantees a plain T back rather than an Optional. <U> U reduce(U identity, BiFunction<U, T, U> accumulator, BinaryOperator<U> combiner) lets the accumulated type U differ entirely from the stream's element type T, with the extra combiner argument used to merge partial results computed on separate threads when the stream runs in parallel.

Why reduce() Was Introduced

Combining every element of a collection into one running result used to mean a loop and a mutable variable tracked by hand across every iteration.

// File: BeforeReduce.java import java.util.*; public class BeforeReduce { public static void main(String[] args) { List<Double> lineTotals = List.of(799.0, 1499.0, 499.0); double cartTotal = 0; for (double lineTotal : lineTotals) { cartTotal += lineTotal; } System.out.println("Cart total: " + cartTotal); } }
Output:
Cart total: 2797.0

reduce() keeps the exact same accumulation logic but removes the mutable loop variable entirely — the identity value and the combining rule are the only two things that change between one reduction and the next.

// File: AfterReduce.java import java.util.*; public class AfterReduce { public static void main(String[] args) { List<Double> lineTotals = List.of(799.0, 1499.0, 499.0); double cartTotal = lineTotals.stream() .reduce(0.0, Double::sum); System.out.println("Cart total: " + cartTotal); } }
Output:
Cart total: 2797.0

Both versions land on the same total. The stream version has no variable being reassigned in a loop for a reader to track by hand.

Syntax

Each overload trades off differently between requiring an identity, returning an Optional, and allowing the accumulated type to differ from the element type.

// File: ReduceSyntaxForms.java import java.util.*; public class ReduceSyntaxForms { public static void main(String[] args) { List<Integer> quantities = List.of(3, 5, 12, 8); // No identity - returns Optional<T> since the stream could be empty Optional<Integer> noIdentityResult = quantities.stream() .reduce((first, second) -> first + second); // With identity - always returns T, identity is the fallback for an empty stream int withIdentityResult = quantities.stream() .reduce(0, (first, second) -> first + second); // Three-argument form - accumulated type differs from the element type int totalDigitLength = quantities.stream() .reduce(0, (partialLength, quantity) -> partialLength + String.valueOf(quantity).length(), Integer::sum); System.out.println("No identity: " + noIdentityResult.orElse(-1)); System.out.println("With identity: " + withIdentityResult); System.out.println("Total digit length: " + totalDigitLength); } }
Output:
No identity: 28
With identity: 28
Total digit length: 5

Common Use Cases

Summing Values With reduce()

reduce() with an identity value works as a general summing mechanism, and it produces the same result Stream.sum()-style convenience methods do — because they are built on the same underlying idea.

// File: SumWithReduceExample.java import java.util.*; public class SumWithReduceExample { public static void main(String[] args) { List<Integer> ratings = List.of(4, 5, 3, 5, 4); int totalViaReduce = ratings.stream().reduce(0, Integer::sum); int totalViaSum = ratings.stream().mapToInt(Integer::intValue).sum(); System.out.println("Total via reduce: " + totalViaReduce); System.out.println("Total via sum: " + totalViaSum); } }
Output:
Total via reduce: 21
Total via sum: 21

Finding an Extreme Value With BinaryOperator.maxBy

BinaryOperator.maxBy combined with a Comparator is the standard idiom for finding the largest element through reduce(), most useful when working with objects rather than plain numbers.

// File: MaxWithReduceExample.java import java.util.*; import java.util.function.*; public class MaxWithReduceExample { record Product(String name, double price) {} public static void main(String[] args) { List<Product> products = List.of( new Product("Mouse", 799.0), new Product("Monitor", 8999.0), new Product("Keyboard", 1499.0) ); Optional<Product> mostExpensive = products.stream() .reduce(BinaryOperator.maxBy(Comparator.comparingDouble(Product::price))); System.out.println(mostExpensive.map(Product::name).orElse("none")); } }
Output:
Monitor

Combining Non-Numeric Values

reduce() is not limited to arithmetic — anything with a sensible way to combine two values into one qualifies, including strings.

// File: StringConcatenationReduceExample.java import java.util.*; public class StringConcatenationReduceExample { public static void main(String[] args) { List<String> tags = List.of("electronics", "sale", "new-arrival"); String combined = tags.stream() .reduce((first, second) -> first + ", " + second) .orElse(""); System.out.println(combined); } }
Output:
electronics, sale, new-arrival

Accumulating Into a Different Type With the Three-Argument Form

The three-argument overload is what makes it possible to reduce a stream of one type into a result of a completely different type, such as reducing a stream of strings into a single character count.

// File: ThreeArgReduceExample.java import java.util.*; public class ThreeArgReduceExample { public static void main(String[] args) { List<String> tags = List.of("electronics", "sale", "new-arrival"); int totalCharacters = tags.stream() .reduce(0, (partialCount, tag) -> partialCount + tag.length(), Integer::sum); System.out.println("Total characters: " + totalCharacters); } }
Output:
Total characters: 26

Basic Example

import java.util.List; public class Main { public static void main(String[] args) { List<Integer> numbers = List.of(1, 2, 3, 4, 5); int sum = numbers.stream() .reduce(0, (total, n) -> total + n); System.out.println("Sum: " + sum); } }

Output

Sum: 15

How it works:

  1. ›0 is the identity value — the starting point for the accumulation, and also what reduce() would return if the stream were empty.
  2. ›(total, n) -> total + n is the accumulator — it runs once per element, combining the running total so far with the next element.
  3. ›reduce() folds the whole stream down to one value: 0+1=1, then 1+2=3, then 3+3=6, then 6+4=10, then 10+5=15, producing the final sum 15.

Combining reduce() With Other Features

reduce() takes a BinaryOperator<T>, itself a specialized Function, so everything about the Function article's composition ideas underlies how an accumulator is written. BinaryOperator.maxBy and minBy, paired with a Comparator, are the standard way to find an extreme value through reduce() when a plain Stream.max() call is not descriptive enough on its own. reduce() and collect() overlap conceptually, but collect() is generally preferred for building a mutable container like a List or a Map, since reduce()'s contract assumes the accumulator has no side effects and produces a fresh result each time — exactly the assumption collect() was designed around handling more efficiently instead.

Best Practices

Reach for a dedicated method — sum(), max(), min(), count() — before reaching for reduce() whenever one already exists. reduce() is the general-purpose fallback for when the aggregation actually needed is not already covered by a purpose-built method.

Keep the accumulator passed to reduce() associative wherever possible — the result should not depend on the order elements happen to be combined in, since a parallel stream is free to group and combine elements in whatever order is convenient for it.

Supply an identity value whenever the stream could plausibly be empty and a sensible fallback other than Optional.empty() exists. Reaching for the no-identity overload out of habit means unwrapping an Optional at every call site, even when a clear default value was available all along.

Use the three-argument form only when the accumulated type genuinely differs from the stream's element type, and remember that its combiner exists purely to support parallel execution — it plays no role at all when the stream is sequential.

Common Mistakes

Using a non-associative accumulator, like subtraction, produces a result that depends entirely on the order elements are combined in — safe and predictable on a sequential stream, but not guaranteed to match if the exact same reduction ever runs in parallel.

// File: NonAssociativeReduceMistake.java import java.util.*; public class NonAssociativeReduceMistake { public static void main(String[] args) { List<Integer> values = List.of(10, 3, 2); // Sequential reduce always folds left to right: (10 - 3) - 2 = 5 int sequentialResult = values.stream() .reduce((first, second) -> first - second) .orElse(0); System.out.println("Sequential result: " + sequentialResult); System.out.println("Subtraction is not associative - a parallel version of this"); System.out.println("same reduce is not guaranteed to produce the same result,"); System.out.println("because elements may be combined in a different grouping"); } }
Output:
Sequential result: 5
Subtraction is not associative - a parallel version of this
same reduce is not guaranteed to produce the same result,
because elements may be combined in a different grouping

Forgetting that the no-identity overload of reduce() returns Optional<T>, not T, is a common compile-time surprise for anyone new to it.

// File: ReduceOptionalMistake.java import java.util.*; public class ReduceOptionalMistake { public static void main(String[] args) { List<Integer> emptyList = List.of(); // int total = emptyList.stream().reduce(Integer::sum); // This does not compile - reduce() without an identity returns // Optional<Integer>, not int, since an empty stream has nothing to give back Optional<Integer> total = emptyList.stream().reduce(Integer::sum); System.out.println("Total present: " + total.isPresent()); System.out.println("Total or default: " + total.orElse(0)); } }
Output:
Total present: false
Total or default: 0

Using reduce() to build a mutable container, like adding elements to an ArrayList inside the accumulator, technically works but goes against how reduce() is designed. The JDK documentation itself discourages this pattern specifically because reduce() assumes each step produces a fresh, independent result — repeatedly mutating and returning the same shared container inside it fights that assumption and performs worse than collect(), which exists precisely for accumulating into a mutable structure.

Interview Questions

Q1. What does reduce() do, and what are its three overloaded forms?

reduce() combines every element of a stream into a single result by repeatedly applying a combining function. The three forms are: reduce(BinaryOperator<T>), which returns Optional<T> with no starting value; reduce(T identity, BinaryOperator<T>), which always returns a plain T using the identity as both a starting point and a fallback; and reduce(U identity, BiFunction<U,T,U>, BinaryOperator<U>), which allows the accumulated type to differ from the element type, with the extra combiner used for merging results across parallel threads.

Q2. What is the difference between the identity and no-identity versions of reduce()?

The identity version takes a starting value that both seeds the accumulation and serves as the result for an empty stream, always returning a plain T. The no-identity version has no such starting point, so it returns Optional<T> instead — there would be no sensible value to return for an empty stream otherwise. Interviewers commonly use this as a quick check for whether a candidate understands why the return type differs between the two overloads rather than treating it as an arbitrary API inconsistency.

Q3. Why does the no-identity overload of reduce() return an Optional?

Because without a supplied starting value, an empty stream genuinely has nothing to reduce, and reduce() cannot invent a result out of nothing. Wrapping the result in Optional makes that absence explicit and forces the caller to decide what an empty stream should mean in their specific case, rather than silently returning null or throwing an unexpected exception.

Q4. What is the purpose of the third argument (the combiner) in the three-argument reduce() overload?

The combiner merges two partial results that were each accumulated independently, which only happens when the stream is processed in parallel across multiple threads. On a sequential stream, the accumulator alone processes every element one at a time and the combiner is never invoked at all — it still has to be supplied because the method signature requires it, purely to support the parallel case correctly if the stream ever becomes one.

Q5. Why should the accumulator passed to reduce() be associative?

Because a parallel stream is free to split the elements into arbitrary groups, reduce each group independently, and then merge the partial results using the combiner — and that only produces a correct, consistent answer if grouping the elements differently does not change the final result. A non-associative accumulator, like subtraction, can produce a different answer depending entirely on how the elements happened to be grouped, which is exactly the kind of bug that only shows up once code that worked fine sequentially is switched to run in parallel.

Q6. When would you use reduce() instead of a dedicated method like sum() or collect()?

Use reduce() when the aggregation needed genuinely is not covered by an existing purpose-built method — combining strings, finding an extreme value by a custom rule with maxBy, or applying a variable-length sequence of rules to a running value, as the checkout example above does. Reach for sum(), max(), or count() first when they already do exactly what is needed, and reach for collect() instead of reduce() whenever the goal is actually building a mutable container like a List or a Map.

FAQs

Is reduce() a terminal or intermediate operation?

Terminal. reduce() consumes the entire stream and produces a single final result — either a plain value or an Optional — and nothing can be chained after it.

Can reduce() be used to build a List or a Map?

Technically yes, but it is discouraged. reduce() assumes each combining step is independent and side-effect-free, while building a List or Map inside it usually means mutating and returning the same shared container repeatedly, which fights that assumption. collect() is the operation actually designed for accumulating into a mutable structure.

What is the difference between reduce() and Collectors.reducing()?

Stream.reduce() is a method called directly on a stream. Collectors.reducing() wraps the same reduction logic as a Collector, which is useful specifically when a reduction needs to happen as part of a larger collect() operation, such as reducing within each group produced by Collectors.groupingBy().

Does reduce() work the same way on a parallel stream as on a sequential one?

The final result is the same as long as the accumulator (and combiner, for the three-argument form) are associative, but the mechanics differ — a sequential stream folds elements strictly left to right, while a parallel stream splits the work, reduces each part independently, and then merges the partial results together, potentially in a different grouping than a strict left-to-right fold would use.

What happens if the accumulator passed to reduce() has side effects?

The result becomes unreliable, especially on a parallel stream, since the accumulator may run multiple times on different threads and in an order that is not guaranteed. reduce()'s contract assumes the accumulator is a pure function of its two inputs, and side effects break that assumption in ways that can produce inconsistent results between runs.

Can reduce() be used on an IntStream directly?

Yes. IntStream, LongStream, and DoubleStream each provide their own reduce() overloads working with IntBinaryOperator, LongBinaryOperator, and DoubleBinaryOperator respectively, avoiding the boxing cost that reducing a boxed Stream<Integer> would carry.

Is the identity value in reduce() required to be a neutral element, like 0 for addition?

It is strongly expected to be, even though the compiler does not enforce it. The identity value is meant to have no effect when combined with any other value — 0 for addition, 1 for multiplication, an empty string for concatenation — and using a non-neutral identity produces a result that is technically computable but rarely means what the code intends.

Summary

reduce() folds a stream down to one value using whatever combining rule the situation calls for — a sum, a maximum found through Comparator, a concatenated string, or a running total pushed through a variable list of rules, exactly as the cart checkout example demonstrates. The three overloads exist purely to answer one question each: does an identity exist, and does the accumulated type need to differ from the element type.

The two habits worth keeping are checking for a dedicated method before reaching for reduce()'s generality, and keeping the accumulator associative, since that single property is what keeps a reduction's result identical whether it runs sequentially or in parallel. collect() picks up from here for the case reduce() was never meant to handle well — building up a mutable result like a List or a Map instead of a single combined value.

What to Read Next

Practice Programs

Put this topic into practice — each program has full code, output and a step-by-step explanation you can run online.

Browse all Java programs →