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.

1// File: BeforeReduce.java 2import java.util.*; 3 4public class BeforeReduce { 5 public static void main(String[] args) { 6 List<Double> lineTotals = List.of(799.0, 1499.0, 499.0); 7 8 double cartTotal = 0; 9 for (double lineTotal : lineTotals) { 10 cartTotal += lineTotal; 11 } 12 13 System.out.println("Cart total: " + cartTotal); 14 } 15}
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.

1// File: AfterReduce.java 2import java.util.*; 3 4public class AfterReduce { 5 public static void main(String[] args) { 6 List<Double> lineTotals = List.of(799.0, 1499.0, 499.0); 7 8 double cartTotal = lineTotals.stream() 9 .reduce(0.0, Double::sum); 10 11 System.out.println("Cart total: " + cartTotal); 12 } 13}
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.

1// File: ReduceSyntaxForms.java 2import java.util.*; 3 4public class ReduceSyntaxForms { 5 public static void main(String[] args) { 6 List<Integer> quantities = List.of(3, 5, 12, 8); 7 8 // No identity - returns Optional<T> since the stream could be empty 9 Optional<Integer> noIdentityResult = quantities.stream() 10 .reduce((first, second) -> first + second); 11 12 // With identity - always returns T, identity is the fallback for an empty stream 13 int withIdentityResult = quantities.stream() 14 .reduce(0, (first, second) -> first + second); 15 16 // Three-argument form - accumulated type differs from the element type 17 int totalDigitLength = quantities.stream() 18 .reduce(0, (partialLength, quantity) -> partialLength + String.valueOf(quantity).length(), Integer::sum); 19 20 System.out.println("No identity: " + noIdentityResult.orElse(-1)); 21 System.out.println("With identity: " + withIdentityResult); 22 System.out.println("Total digit length: " + totalDigitLength); 23 } 24}
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.

1// File: SumWithReduceExample.java 2import java.util.*; 3 4public class SumWithReduceExample { 5 public static void main(String[] args) { 6 List<Integer> ratings = List.of(4, 5, 3, 5, 4); 7 8 int totalViaReduce = ratings.stream().reduce(0, Integer::sum); 9 int totalViaSum = ratings.stream().mapToInt(Integer::intValue).sum(); 10 11 System.out.println("Total via reduce: " + totalViaReduce); 12 System.out.println("Total via sum: " + totalViaSum); 13 } 14}
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.

1// File: MaxWithReduceExample.java 2import java.util.*; 3import java.util.function.*; 4 5public class MaxWithReduceExample { 6 record Product(String name, double price) {} 7 8 public static void main(String[] args) { 9 List<Product> products = List.of( 10 new Product("Mouse", 799.0), 11 new Product("Monitor", 8999.0), 12 new Product("Keyboard", 1499.0) 13 ); 14 15 Optional<Product> mostExpensive = products.stream() 16 .reduce(BinaryOperator.maxBy(Comparator.comparingDouble(Product::price))); 17 18 System.out.println(mostExpensive.map(Product::name).orElse("none")); 19 } 20}
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.

1// File: StringConcatenationReduceExample.java 2import java.util.*; 3 4public class StringConcatenationReduceExample { 5 public static void main(String[] args) { 6 List<String> tags = List.of("electronics", "sale", "new-arrival"); 7 8 String combined = tags.stream() 9 .reduce((first, second) -> first + ", " + second) 10 .orElse(""); 11 12 System.out.println(combined); 13 } 14}
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.

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

Real-World Example

An e-commerce checkout service typically applies several discount rules to a cart's raw total one after another — a festive percentage discount, a flat coupon deduction, a minimum payable floor — and the number and combination of active rules changes often as marketing campaigns come and go. Folding the raw total through a list of rules with reduce() means the checkout logic itself never has to change when the rules do.

1// File: CartCheckoutService.java 2import java.util.*; 3import java.util.function.*; 4 5public class CartCheckoutService { 6 7 public double calculateFinalTotal(List<DoubleUnaryOperator> discountRules, double rawTotal) { 8 return discountRules.stream() 9 .reduce(rawTotal, 10 (amount, rule) -> rule.applyAsDouble(amount), 11 (amount1, amount2) -> amount2); 12 } 13}
1// File: CartCheckoutDemo.java 2import java.util.*; 3import java.util.function.*; 4 5public class CartCheckoutDemo { 6 public static void main(String[] args) { 7 List<DoubleUnaryOperator> discountRules = List.of( 8 amount -> amount * 0.9, // 10% festive discount 9 amount -> amount - 100, // flat coupon deduction 10 amount -> Math.max(amount, 200) // minimum payable floor 11 ); 12 13 CartCheckoutService checkoutService = new CartCheckoutService(); 14 15 double rawTotal = 2797.0; 16 double finalTotal = checkoutService.calculateFinalTotal(discountRules, rawTotal); 17 18 System.out.println("Raw total: Rs." + String.format("%.2f", rawTotal)); 19 System.out.println("Final total after discounts: Rs." + String.format("%.2f", finalTotal)); 20 } 21}
Output:
Raw total: Rs.2797.00
Final total after discounts: Rs.2417.30

The third argument to reduce() here, (amount1, amount2) -> amount2, is the combiner used only when a stream runs in parallel and partial results computed on different threads need to be merged back together. On a sequential stream like this one it is never actually invoked — it still has to be supplied, though, because the three-argument overload requires it regardless of whether the stream ever runs in parallel. During code reviews, seniors commonly flag a checkout method that hardcodes each discount step directly inside calculateFinalTotal instead of folding over a list of rules, because every new discount campaign then means editing that method again — reduce() over a list of rules means the rule set can grow or shrink without CartCheckoutService itself ever changing.

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.

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

1// File: ReduceOptionalMistake.java 2import java.util.*; 3 4public class ReduceOptionalMistake { 5 public static void main(String[] args) { 6 List<Integer> emptyList = List.of(); 7 8 // int total = emptyList.stream().reduce(Integer::sum); 9 // This does not compile - reduce() without an identity returns 10 // Optional<Integer>, not int, since an empty stream has nothing to give back 11 12 Optional<Integer> total = emptyList.stream().reduce(Integer::sum); 13 System.out.println("Total present: " + total.isPresent()); 14 System.out.println("Total or default: " + total.orElse(0)); 15 } 16}
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