
reduce() is a terminal operation of the Java Stream API that combines all elements of a stream into a single value – a sum, a product, a minimum, or any other result. To do so, it uses a function you pass in to combine two values into one – again and again, until only one is left.
This is how reduce() adds up the numbers from 1 to 5:
int sum = IntStream.rangeClosed(1, 5)
.reduce(0, (a, b) -> a + b);
15
reduce() has existed since Java 8, in three variants. In this article, I show you which one to use when, why the first argument must not be just any start value, and when collect() is the better tool.
In this article, you will find out
- how
reduce()works step by step, - which three variants of
reduce()exist and what they return for an empty stream, - what to use
reduce()for – and for which tasks there are ready-made operations, - which three rules
reduce()depends on in a parallel stream, - when
collect()is the better tool, - how
reduce()differs from the gathererfold(), - which mistakes to avoid.
The Examples in This Article
The examples use the data model of the article about Java Streams – an enum Genre, a record Book, and a small library of eleven classics:
public enum Genre {
NOVEL,
GOTHIC,
ADVENTURE,
FANTASY,
SCIENCE_FICTION
}
public record Book(String title, String author, int year, Genre genre) {}
public class Library {
public static final List<Book> BOOKS = List.of(
new Book("Pride and Prejudice", "Jane Austen", 1813, NOVEL),
new Book("Frankenstein", "Mary Shelley", 1818, GOTHIC),
new Book("Moby-Dick", "Herman Melville", 1851, ADVENTURE),
new Book("From the Earth to the Moon", "Jules Verne", 1865, SCIENCE_FICTION),
new Book("Alice's Adventures in Wonderland", "Lewis Carroll", 1865, FANTASY),
new Book("Around the World in Eighty Days", "Jules Verne", 1873, ADVENTURE),
new Book("Treasure Island", "Robert Louis Stevenson", 1883, ADVENTURE),
new Book("Kidnapped", "Robert Louis Stevenson", 1886, ADVENTURE),
new Book("The Time Machine", "H. G. Wells", 1895, SCIENCE_FICTION),
new Book("Dracula", "Bram Stoker", 1897, GOTHIC),
new Book("The War of the Worlds", "H. G. Wells", 1898, SCIENCE_FICTION));
}
You can find the complete code of all examples in the GitHub repository java-streams-examples, in the package eu.happycoders.reduce.
How Does reduce() Work?
In its simplest form, reduce() takes two arguments: a value the calculation starts with, and a function that combines two values into one. In the example from the introduction, these are the 0 and the function (a, b) -> a + b.
reduce() first calls the function with the 0 and the first element, then with this intermediate result and the second element, and so on up to the last element. The result of the last call is the result of reduce(). For the numbers from 1 to 5, this corresponds to the following loop:
int result = 0;
for (int element = 1; element <= 5; element++) {
result = result + element;
}
You can see the individual calls of the reduce() function in the stream if you let the function print each call:
int sum = IntStream.rangeClosed(1, 5)
.reduce(0, (a, b) -> {
System.out.println(a + " + " + b + " = " + (a + b));
return a + b;
});
0 + 1 = 1
1 + 2 = 3
3 + 3 = 6
6 + 4 = 10
10 + 5 = 15
In each call, the first parameter a is the intermediate result so far, the second parameter b the next element.
The output in the function is only there to illustrate the calls. In a parallel stream, the lines would come in a different order, as the section on parallel streams shows.
The two arguments of reduce() have names you will meet again in the Javadoc and in the rest of this article:
- The first argument,
identity, is the identity – here the0. An identity is a value that does not change the result of the function:0 + xyieldsxagain for everyx. For a multiplication, it is the1; for joining strings, the empty string. Whyreduce()requires such a value and not just any start value is shown in the section on parallel streams. - The second argument,
accumulator, is the accumulator – the function that combines an intermediate result and the next element into the new intermediate result, here(a, b) -> a + b.
The following diagram shows the process: the elements at the top, the intermediate results at the bottom, starting with the identity. Each further intermediate result is created from the previous one and the element above it.
The Javadoc calls this combining of all elements into one value a reduction, also known as a fold.
The Three Variants of reduce()
The Stream interface declares reduce() in three variants:
T reduce(T identity, BinaryOperator<T> accumulator)
Optional<T> reduce(BinaryOperator<T> accumulator)
<U> U reduce(
U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
The following sections show each variant with an example and with what it returns for an empty stream.
reduce(identity, accumulator) – With an Identity
You already know the first variant from the previous section. Its accumulator is a BinaryOperator<T>: it combines two values of the element type into a third one of the same type. The result therefore also has the element type.
The following example maps each book to the length of its title and adds up the lengths:
int totalLength = BOOKS.stream()
.map(book -> book.title().length())
.reduce(0, Integer::sum);
197
Integer::sum is a method reference to the static method Integer.sum(int a, int b), which does the same as (a, b) -> a + b.
For an empty stream, this variant returns the identity.
No book in the library was published before 1800, so the following example returns the 0:
int noLength = BOOKS.stream()
.filter(book -> book.year() < 1800)
.map(book -> book.title().length())
.reduce(0, Integer::sum);
0
reduce(accumulator) – Without an Identity
Not every combination has a sensible identity. That is why reduce() also exists without an identity.
The following example looks for the book with the longest title. The accumulator compares two books and returns the one with the longer title – the first one if both are equally long:
Optional<Book> longest = BOOKS.stream()
.reduce((a, b) -> a.title().length() >= b.title().length() ? a : b);
System.out.println(longest.map(Book::title));
Optional[Alice's Adventures in Wonderland]
There is no sensible identity here. You would have to invent one: for example, a book with an empty title that loses against every other – and that would end up in the result for an empty stream.
Without an identity, reduce() starts with the first element as the intermediate result and combines it with the second. An empty stream has no first element, so this variant returns an Optional – and it is empty if the stream is empty.
With the filter from the previous section, no book is left, and reduce() returns an empty Optional:
Optional<Book> none = BOOKS.stream()
.filter(book -> book.year() < 1800)
.reduce((a, b) -> a.title().length() >= b.title().length() ? a : b);
System.out.println(none.map(Book::title));
Optional.empty
reduce(identity, accumulator, combiner) – With a Different Result Type
In the first two variants, the elements and the result have the same type. That is why the title-length example first had to turn the books into numbers with map().
The third variant allows a result of type U that differs from the element type T. Its accumulator is therefore not a BinaryOperator<T> but a BiFunction<U, ? super T, U>: it combines an intermediate result of type U with an element of type T.
The following example adds up the title lengths directly from the books, without map():
int totalLength = BOOKS.stream()
.reduce(0, (sum, book) -> sum + book.title().length(), Integer::sum);
197
The third argument is the combiner – here Integer::sum. It combines two intermediate results of type U. The accumulator cannot do that, because it expects a book as its second argument, not a number.
In a sequential stream, reduce() never calls the combiner. It is only needed in a parallel stream, which produces several intermediate results – what happens there is shown in the section on parallel streams.
The Javadoc of the package java.util.stream recommends the combination of map() and reduce() with two arguments over this variant for most cases, because it is more readable. According to the Javadoc, the three-argument variant is meant for cases in which merging the mapping and the combining into one function saves significant work.
I therefore recommend map() and reduce() – and for a result that is a container such as a list or a StringBuilder, collect(), as the section reduce() vs. collect() shows.
reduce() on IntStream, LongStream, and DoubleStream
The primitive streams only have the first two variants, with primitive types: on an IntStream, these are int reduce(int identity, IntBinaryOperator op) and OptionalInt reduce(IntBinaryOperator op). There is no variant with a combiner.
The following example adds up the title lengths without the detour via Integer objects and finds the longest title length:
int totalLength = BOOKS.stream()
.mapToInt(book -> book.title().length())
.reduce(0, Integer::sum);
OptionalInt maxLength = BOOKS.stream()
.mapToInt(book -> book.title().length())
.reduce(Math::max);
197
OptionalInt[32]
Both calls have ready-made counterparts, sum() and max() – and internally, these consist of exactly these calls: the JDK implements IntStream.sum() as reduce(0, Integer::sum) and IntStream.max() as reduce(Math::max).
The following table summarizes the variants. LongStream and DoubleStream correspond to IntStream, with long or double and OptionalLong or OptionalDouble:
| Variant | Return type | for an empty stream |
|---|---|---|
reduce(identity, accumulator) | T | identity |
reduce(accumulator) | Optional<T> | Optional.empty() |
reduce(identity, accumulator, combiner) | U | identity |
IntStream.reduce(identity, op) | int | identity |
IntStream.reduce(op) | OptionalInt | OptionalInt.empty() |
Examples of Use
Sum and Product
For sums, the primitive streams have sum(). For a product, however, there is no ready-made operation – here, reduce() is the tool, with the 1 as the identity.
The following example calculates the factorial 5! = 1 · 2 · 3 · 4 · 5:
int factorial = IntStream.rangeClosed(1, 5)
.reduce(1, (a, b) -> a * b);
120
With larger numbers, a product quickly exceeds the range of int. How to notice that is shown in the section on overflow.
Minimum and Maximum
The static methods BinaryOperator.minBy() and maxBy() return an accumulator that returns the smaller or larger of two elements according to a Comparator – the first one in case of a tie.
The following example finds the oldest book:
Optional<Book> oldest = BOOKS.stream()
.reduce(BinaryOperator.minBy(Comparator.comparingInt(Book::year)));
System.out.println(oldest.map(Book::title).orElse("-"));
Pride and Prejudice
min() returns the same, because the JDK implements Stream.min() as reduce(BinaryOperator.minBy(comparator)), and max() accordingly with maxBy().
I recommend min() and max(), because their names say what they do:
Optional<Book> oldest = BOOKS.stream()
.min(Comparator.comparingInt(Book::year));
Summing BigDecimal and Your Own Classes
In Java, you calculate money amounts with BigDecimal, because double cannot represent decimal fractions like 0.1 exactly. There is no primitive stream for BigDecimal and therefore no sum() – here, reduce() is the direct way.
The following example adds up the three amounts of an order. BigDecimal.ZERO is the identity, and BigDecimal::add is the accumulator:
List<BigDecimal> prices = List.of(
new BigDecimal("12.99"), new BigDecimal("8.50"), new BigDecimal("14.95"));
BigDecimal total = prices.stream()
.reduce(BigDecimal.ZERO, BigDecimal::add);
36.44
This works the same way with classes you write yourself. The following record Money stores an amount in cents as a long; ZERO is the identity for its method add(), and add() returns a new Money object instead of changing the existing one:
public record Money(long cents) {
public static final Money ZERO = new Money(0);
public Money add(Money other) {
return new Money(cents + other.cents);
}
}
With Money.ZERO as the identity and Money::add as the accumulator, reduce() adds up the same order, this time as Money objects:
List<Money> prices = List.of(new Money(1299), new Money(850), new Money(1495));
Money total = prices.stream()
.reduce(Money.ZERO, Money::add);
Money[cents=3644]
That add() returns a new object is not a detail: why an accumulator that changes its first argument returns wrong results in a parallel stream is shown in the section Building a List with reduce(). A Money class for real applications would also have a currency and would check when adding that both amounts have the same one – that does not change anything about reduce().
Chaining Conditions and Functions
This section shows an exotic way of chaining conditions and functions – you will rarely see it in everyday code. It does, however, make visible that reduce() works not only with numbers but with every type whose values can be combined in pairs – including functional interfaces – and what an identity is beyond numbers.
The following example uses Predicate::and to combine a list of conditions into a single condition that holds when all of them hold:
List<Predicate<Book>> conditions = List.of(
book -> book.genre() == ADVENTURE,
book -> book.year() > 1860,
book -> book.author().startsWith("Robert"));
Predicate<Book> all = conditions.stream()
.reduce(book -> true, Predicate::and);
List<String> titles = BOOKS.stream()
.filter(all)
.map(Book::title)
.toList();
[Treasure Island, Kidnapped]
Here, the identity is the predicate book -> true: combined with and(), it does not change any condition. For Predicate::or, it would be book -> false.
The identity also determines what an empty list of conditions yields. book -> true then lets all eleven books pass – and that is the right answer for “all conditions hold”, because there is no condition a book could violate.
In the same way, you chain a list of functions with Function::andThen, with Function.identity() as the identity:
List<Function<String, String>> steps = List.of(
String::strip,
String::toUpperCase,
title -> title.replace(' ', '_'));
Function<String, String> pipeline = steps.stream()
.reduce(Function.identity(), Function::andThen);
System.out.println(pipeline.apply(" The Time Machine "));
THE_TIME_MACHINE
Still, I do not recommend this form, because it is hard to read: with reduce(book -> true, Predicate::and), you first have to think about what comes out.
If you already know the conditions when writing the code, combine them directly with and():
Predicate<Book> isAdventure = book -> book.genre() == ADVENTURE;
Predicate<Book> after1860 = book -> book.year() > 1860;
Predicate<Book> byRobert = book -> book.author().startsWith("Robert");
List<String> titles = BOOKS.stream()
.filter(isAdventure.and(after1860).and(byRobert))
.map(Book::title)
.toList();
[Treasure Island, Kidnapped]
You chain the functions accordingly with andThen():
Function<String, String> strip = String::strip;
Function<String, String> pipeline = strip
.andThen(String::toUpperCase)
.andThen(title -> title.replace(' ', '_'));
System.out.println(pipeline.apply(" The Time Machine "));
THE_TIME_MACHINE
If the conditions come as a list, allMatch() says more readably what is meant – that every condition must hold:
List<String> titles = BOOKS.stream()
.filter(book -> conditions.stream().allMatch(condition -> condition.test(book)))
.map(Book::title)
.toList();
[Treasure Island, Kidnapped]
Where Ready-Made Operations Exist
For the most common reductions, the Stream API has methods of its own. They are shorter, and their names say what they do. The following table compares them with the solution using reduce():
| Task | with reduce() | better |
|---|---|---|
| Sum | map(…).reduce(0, Integer::sum) | mapToInt(…).sum() |
| Count | map(e -> 1).reduce(0, Integer::sum) | count() |
| Minimum, maximum | reduce(BinaryOperator.minBy(c)) | min(c), max(c) |
| Joining strings | reduce("", String::concat) | collect(joining()) |
For the average, you need the sum and the count at the same time; the primitive streams provide both with average() and summaryStatistics(). Why joining() is more than a matter of style when joining strings is shown in the section Joining Strings with reduce().
With double, sum() is also more precise than reduce(). DoubleStream.sum() adds with compensated summation: it keeps the rounding error of each addition and factors it into the next one. reduce(0, Double::sum), on the other hand, simply adds one value after the other.
The following example adds 0.1, 0.2, and 0.3 both ways:
double reduced = DoubleStream.of(0.1, 0.2, 0.3)
.reduce(0, Double::sum);
double summed = DoubleStream.of(0.1, 0.2, 0.3)
.sum();
0.6000000000000001
0.6
reduce() in Parallel Streams
A parallel stream splits its elements into parts and processes them on several threads. reduce() reduces each part on its own and then combines the partial results with the combiner.
The following example adds up the numbers from 1 to 5 in a parallel stream and prints every call of the accumulator and the combiner. boxed() turns the IntStream into a Stream<Integer>, because only there does the variant with a combiner exist:
int sum = IntStream.rangeClosed(1, 5)
.boxed()
.parallel()
.reduce(
0,
(a, b) -> {
System.out.println("accumulate " + a + " + " + b + " = " + (a + b));
return a + b;
},
(a, b) -> {
System.out.println("combine " + a + " + " + b + " = " + (a + b));
return a + b;
});
accumulate 0 + 5 = 5
accumulate 0 + 4 = 4
accumulate 0 + 3 = 3
combine 4 + 5 = 9
accumulate 0 + 1 = 1
combine 3 + 9 = 12
accumulate 0 + 2 = 2
combine 1 + 2 = 3
combine 3 + 12 = 15
The order of the lines changes from run to run, because the threads work at the same time. What stays the same is which values are combined: the stream has made each of the five elements a part of its own, and each part starts with the identity 0. The combiner then combines the partial results in pairs until one is left.
The following diagram shows the same calls as a tree: the five calls of the accumulator at the top, the four calls of the combiner below them.
For a parallel stream to return the same result as a sequential one, the identity, the accumulator, and the combiner must follow three rules. None of them is checked – a violation only shows when the same code runs in parallel.
Rule 1: The Identity Must Not Change the Result
The identity is not a start value that goes into the result once, because every part starts with it. If you want to add 10 to the sum of the numbers from 1 to 5 with reduce(10, Integer::sum), you get the expected result sequentially – and a different one in parallel:
int sequential = IntStream.rangeClosed(1, 5)
.reduce(10, Integer::sum);
int parallel = IntStream.rangeClosed(1, 5)
.parallel()
.reduce(10, Integer::sum);
25
65
In the parallel stream, each of the five parts starts with the 10 – so 5 · 10 = 50 instead of 10 go into the sum, and 25 becomes 65.
The identity must therefore be a value that does not change the result, no matter how often it goes into it: 0 for a sum, 1 for a product, the empty string for joining strings. The Javadoc puts it like this: accumulator.apply(identity, t) must be equal to t for every t.
You add a real start value outside of reduce():
int sum = 10 + IntStream.rangeClosed(1, 5)
.parallel()
.reduce(0, Integer::sum);
25
Rule 2: The Accumulator Must Be Associative
An accumulator is associative if it does not matter where you put the parentheses: (a op b) op c must yield the same as a op (b op c). That holds for the addition and multiplication of integers, for minimum, maximum, and joining strings. The parallel stream makes use of this freedom – in the tree above, it combined 4 and 5 before it got to the 3.
The following example tries to build the number 12345 from the digits 1 to 5. To do so, the accumulator shifts the intermediate result one decimal place to the left and appends the next digit:
int sequential = IntStream.rangeClosed(1, 5)
.reduce(0, (a, b) -> 10 * a + b);
int parallel = IntStream.rangeClosed(1, 5)
.parallel()
.reduce(0, (a, b) -> 10 * a + b);
12345
195
The identity 0 satisfies rule 1: 10 * 0 + b yields b. The accumulator, however, is not associative. The parallel stream first combines 4 and 5 into 45 and then the 3 with the 45 – into 10 · 3 + 45 = 75 instead of 345. The accumulator appends a single digit; it does not provide for a multi-digit partial result as its second argument.
You cannot parallelize a reduction with reduce() if its accumulator is not associative. For such a reduction, there is the gatherer fold(), which the section reduce() vs. Gatherers.fold() shows.
Rule 3: The Combiner Must Match the Accumulator
The combiner combines partial results that the accumulator has produced. In doing so, it must arrive at the same result as if the accumulator had processed the elements one after the other. The Javadoc puts it like this: combiner.apply(u, accumulator.apply(identity, t)) must be equal to accumulator.apply(u, t).
The sum of squares shows what happens if that does not hold. The accumulator (sum, x) -> sum + x * x treats its two arguments differently: the first is a sum, the second an element that it squares. That is why it is no use as a combiner – two partial sums must be added without squaring either of them.
In the two-argument variant, however, there is no separate combiner – reduce() uses the accumulator as the combiner as well. The result is correct sequentially, but not in parallel:
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
int sequential = numbers.stream()
.reduce(0, (sum, x) -> sum + x * x);
int parallel = numbers.parallelStream()
.reduce(0, (sum, x) -> sum + x * x);
55
1326867573
For the parts 4 and 5, the accumulator acting as the combiner calculates 16 + 25 · 25 = 641 instead of 16 + 25 = 41. One level further down, it squares the 641 again, and in the end, the square of the partial result 410,890 exceeds the range of int.
The three-argument variant separates the two roles: the accumulator squares and adds, the combiner Integer::sum only adds. It is even more readable to move the squaring into a map() before reduce() – then the accumulator is an ordinary, associative BinaryOperator again:
int withCombiner = numbers.parallelStream()
.reduce(0, (sum, x) -> sum + x * x, Integer::sum);
int withMap = numbers.parallelStream()
.map(x -> x * x)
.reduce(0, Integer::sum);
55
55
Whether a parallel stream pays off at all is a different question – it will be the topic of a separate article.
reduce() vs. collect()
reduce() is an immutable reduction: each call of the accumulator creates a new value and leaves the old ones unchanged – Integer.sum() a new number, BigDecimal.add() a new BigDecimal.
collect(), on the other hand, is a mutable reduction: it collects the elements in a container such as an ArrayList, a HashMap, or a StringBuilder, which it modifies along the way.
Confusing the two leads to code that works sequentially and returns wrong results in parallel – or to code whose effort grows quadratically. The following two sections show one example each.
Building a List with reduce()
The following example collects book titles in an ArrayList with the three-argument variant of reduce(). The identity is an empty list, the accumulator adds a title, and the combiner appends one list to the other:
List<String> titles = BOOKS.stream()
.reduce(
new ArrayList<>(),
(list, book) -> {
list.add(book.title());
return list;
},
(a, b) -> {
a.addAll(b);
return a;
});
Sequentially, this returns the eleven titles. With BOOKS.parallelStream() instead of BOOKS.stream(), the same reduce() returns five different lists in five runs – here, the number of their entries:
3344 titles
912 titles
4776 titles
1344 titles
992 titles
The cause is the identity. reduce() does not create it anew for each part but passes the same object to every part – the one ArrayList you passed in. All threads add to this one list at the same time, and it is not thread-safe. The combiner also appends the list to itself, because a and b are the same object; so each call of the combiner doubles the list.
collect() is made for mutable containers. Its three-argument variant corresponds to the reduce() attempt argument by argument – with one difference: instead of an identity, it takes a Supplier, which it calls for each part. That way, each part has a list of its own:
ArrayList<String> titles = BOOKS.parallelStream()
.collect(
() -> new ArrayList<>(),
(list, book) -> {
list.add(book.title());
},
(a, b) -> {
a.addAll(b);
});
This returns the eleven titles, sequentially as well as in parallel. The accumulator and the combiner are BiConsumers here: they have no return value, because they modify the list instead of creating a new value. That is why the return statements from the reduce() attempt are gone.
You can replace the Supplier and the combiner with the method references ArrayList::new and ArrayList::addAll. The accumulator remains a lambda, because it first has to get the title from the book; it does, however, fit into one line without curly braces:
ArrayList<String> titles = BOOKS.parallelStream()
.collect(
ArrayList::new,
(list, book) -> list.add(book.title()),
ArrayList::addAll);
If you extract the titles with map() first, the accumulator becomes the method reference ArrayList::add as well:
ArrayList<String> titles = BOOKS.parallelStream()
.map(Book::title)
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
For a list, however, toList() is enough; you only need the three-argument variant for a container for which there is no ready-made collector.
Joining Strings with reduce()
Strings are immutable, so reduce() returns a correct result here, also in parallel. The problem is the effort.
The following example joins the eleven titles:
String concatenated = BOOKS.stream()
.map(Book::title)
.reduce("", String::concat);
Each call of concat() creates a new string and copies all previous characters plus those of the next title into it. The eleven titles have 197 characters in total, and 1,222 characters are copied along the way. For 11,000 titles – the library a thousand times in a row – it is 197,000 characters and 1,083,638,500 copied ones. The effort grows quadratically with the number of characters, which the Javadoc of the package java.util.stream warns about using the same example.
Collectors.joining(), in contrast, collects the titles in a mutable container – a StringBuilder without a delimiter, a StringJoiner with one – and its effort grows linearly with the number of characters.
The following example joins the titles with a comma and a space as the delimiter:
String joined = BOOKS.stream()
.map(Book::title)
.collect(joining(", "));
Pride and Prejudice, Frankenstein, Moby-Dick, From the Earth to the Moon, Alice's Adventures in Wonderland, Around the World in Eighty Days, Treasure Island, Kidnapped, The Time Machine, Dracula, The War of the Worlds
When to Use reduce(), When collect()?
The following table summarizes the differences:
reduce() | collect() | |
|---|---|---|
| Result | an immutable value: number, String, BigDecimal, record | a mutable container: List, Map, StringBuilder |
| One step | creates a new value | modifies the container |
| Parallel | every part starts with the same identity | every part gets a container of its own from the Supplier |
I recommend reduce() for results that can be expressed as a single value, and collect() for everything that collects elements.
Collectors.reducing()
A reduction also exists as a collector: Collectors.reducing(). On its own, you do not need it, because collect(reducing(…)) returns the same as reduce(…). It is meant for the places where another collector expects a collector – above all as a downstream collector of groupingBy(), which combines the elements of each group.
The following example finds the oldest book per genre:
Map<Genre, Optional<Book>> oldestPerGenre = BOOKS.stream()
.collect(groupingBy(
Book::genre,
TreeMap::new,
reducing(BinaryOperator.minBy(Comparator.comparingInt(Book::year)))));
oldestPerGenre.forEach((genre, book) ->
System.out.println(genre + ": " + book.map(Book::title).orElse("-")));
NOVEL: Pride and Prejudice
GOTHIC: Frankenstein
ADVENTURE: Moby-Dick
FANTASY: Alice's Adventures in Wonderland
SCIENCE_FICTION: From the Earth to the Moon
Like reduce(), reducing() comes in three variants. The first two, reducing(identity, op) and reducing(op), correspond to those of reduce(). The third, reducing(identity, mapper, op), has a function that maps each element before combining it instead of a combiner – so it unites map() and reduce() in one collector.
For the minimum, there is a ready-made collector here as well: Collectors.minBy(), which the JDK implements as reducing(BinaryOperator.minBy(comparator)). groupingBy() and its downstream collectors will be the topic of a separate article.
reduce() vs. Gatherers.fold()
Since Java 24, the gatherer Gatherers.fold() has provided a second form of reduction. It differs from reduce() in three points:
fold()is an intermediate operation and returns a stream with exactly one element; the pipeline can continue after it.fold()does not need a combiner, even if the result has a different type than the elements.fold()always processes the elements one after the other, even in a parallel stream. The accumulator therefore does not have to be associative.
That is why, with fold(), even a parallel stream correctly builds the number from the digits in rule 2:
int number = IntStream.rangeClosed(1, 5)
.boxed()
.parallel()
.gather(Gatherers.fold(() -> 0, (a, b) -> 10 * a + b))
.findFirst()
.orElseThrow();
12345
That fold() calculates correctly even with a non-associative accumulator comes at a price: even a parallel stream does not process the fold() step in parallel. You can find a comparison of reduce(), fold(), and scan() in the article about Stream Gatherers.
Common Mistakes
get() on an Empty Optional
reduce() without an identity returns an Optional, and on an empty Optional, get() throws a NoSuchElementException:
Book oldest = BOOKS.stream()
.filter(book -> book.year() < 1800)
.reduce((a, b) -> a.year() <= b.year() ? a : b)
.get();
java.util.NoSuchElementException: No value present
If the stream can be empty, I recommend orElse() with a fallback value. If an empty stream is an error, use orElseThrow(): it throws the same exception, but its name says that this is intended. The article about Java Optional shows further ways.
Stream<Integer> Instead of IntStream
reduce(0, Integer::sum) on a Stream<Integer> boxes each intermediate result into an Integer, because Integer::sum returns an int, but the accumulator has to return an Integer. The boxing is done by Integer.valueOf(), which by default keeps ready-made objects only for the values from −128 to 127; for every larger value, it creates a new one.
For the sum of the eleven title lengths, that makes six new Integer objects, because the intermediate sum exceeds 127 after the sixth book. For a million titles, it would be almost a million new objects.
mapToInt() and sum(), on the other hand, calculate with int throughout:
int totalLength = BOOKS.stream()
.mapToInt(book -> book.title().length())
.sum();
Overflow in Sums and Products
int and long silently overflow in sums and products. The factorial of 13 is 6,227,020,800 and no longer fits into an int, whose largest value is 2,147,483,647. reduce() still returns a result – just a wrong one:
int factorial = IntStream.rangeClosed(1, 13)
.reduce(1, (a, b) -> a * b);
1932053504
With Math::multiplyExact as the accumulator, you get an exception in case of an overflow instead; for sums, there is Math::addExact accordingly:
int factorial = IntStream.rangeClosed(1, 13)
.reduce(1, Math::multiplyExact);
java.lang.ArithmeticException: integer overflow
The same applies to sum(), which internally calls reduce(0, Integer::sum): it overflows just as silently. A LongStream reaches up to the factorial of 20; for larger values, you need BigInteger and reduce(BigInteger.ONE, BigInteger::multiply).
Summary
reduce() combines the elements of a stream into one value by applying an accumulator again and again to an intermediate result and the next element. With an identity, it returns precisely this identity for an empty stream; without an identity, it returns an Optional. The variant with a combiner allows a result of a different type than the elements.
In a parallel stream, each part starts with the identity, and the combiner combines the partial results. For that to be correct, the identity must not change the result, the accumulator must be associative, and the combiner must combine partial results the way the accumulator combines elements.
Four recommendations for everyday work:
- Use the ready-made operations where they exist:
sum(),count(),min(),max(),average(). - Use
reduce()for reductions without a ready-made operation: a product, a sum ofBigDecimalamounts or of objects of a class of your own likeMoney. - Write
map()andreduce()instead of the three-argument variant where possible. - Use
collect()as soon as the result is a container – a list, a map, aStringBuilder.
How reduce() fits into a stream pipeline and which other operations exist is shown in the article about Java Streams.
Was this article helpful to you? Then I’d be happy if you took a moment to leave a review on my ProvenExpert profile.




