1. The reduce method: a universal reduction
In programming, you often need to “fold” a collection into a single result: compute a sum, find a product, join strings, compute an aggregated metric, or assemble elements into a new structure. Previously this was done manually with loops and an accumulator variable. Today the Stream API offers elegant ways — the universal methods reduce and collect, which let you write code that is compact and declarative.
- reduce — folds a stream into a single result (sum, product, concatenation, etc.).
- collect — transforms a stream into a collection, string, map, or an arbitrary structure.
Let’s go step by step.
Why do we need reduce?
reduce is a terminal method that “folds” stream elements into a single value using an accumulator function. Conceptually, it’s like iterating over the collection while incrementally accumulating the result.
Method signatures of reduce
The Stream API has three main variants of reduce():
Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
- accumulator — a function that takes the current accumulated value and the next element and returns a new result.
- identity — the initial value of the accumulator (for example, 0 for sum, 1 for product).
- combiner — used in parallel streams to merge intermediate results.
Examples of using reduce
Example 1: Sum of numbers
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// reduce without identity — result is Optional
Optional<Integer> sum1 = numbers.stream()
.reduce((a, b) -> a + b);
System.out.println(sum1.orElse(0)); // 15
// reduce with identity — result is always present
int sum2 = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println(sum2); // 15
Example 2: Product of all numbers
int product = numbers.stream()
.reduce(1, (a, b) -> a * b);
System.out.println(product); // 120
Example 3: String concatenation
List<String> words = List.of("Java", "Stream", "API");
String phrase = words.stream()
.reduce("", (a, b) -> a + " " + b);
System.out.println(phrase.trim()); // Java Stream API
Example 4: Finding the maximum element
Optional<Integer> max = numbers.stream()
.reduce(Integer::max);
max.ifPresent(System.out::println); // 5
Example 5: Sum of lengths of all strings
List<String> texts = List.of("cat", "dog", "elephant");
int totalLength = texts.stream()
.map(String::length)
.reduce(0, Integer::sum);
System.out.println(totalLength); // 14
How reduce works
The logic of reduce is equivalent to the following loop:
T result = identity;
for (T element : collection) {
result = accumulator.apply(result, element);
}
return result;
If identity is not provided, the first element of the stream is used as the starting value, and the method returns an Optional (it may be empty if the stream is empty).
2. The collect method: a universal transformation
collect is a terminal method that turns a stream into a collection, string, map, or any other structure. This uses “collectors” (Collector) that describe the assembly process. Most often, we use the ready-made ones from the Collectors class.
Most popular collectors
- Collectors.toList() — collects elements into a List.
- Collectors.toSet() — collects elements into a Set.
- Collectors.toMap() — collects elements into a Map.
- Collectors.joining() — joins strings into one.
- Collectors.groupingBy() — groups elements by a classifier.
- Collectors.counting() — counts elements.
- Collectors.summarizingInt() — collects statistics over numbers (sum, average, min/max).
Examples of using collect
Example 1: Collect into a list
List<String> names = List.of("Alice", "Bob", "Charlie", "Alice");
List<String> uniqueNames = names.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(uniqueNames); // [Alice, Bob, Charlie]
Example 2: Collect into a set (Set)
Set<String> nameSet = names.stream()
.collect(Collectors.toSet());
System.out.println(nameSet); // [Alice, Bob, Charlie] (order is not guaranteed)
Example 3: Collect into a string
String csv = names.stream()
.collect(Collectors.joining(", "));
System.out.println(csv); // Alice, Bob, Charlie, Alice
Example 4: Collect into a Map
Suppose we have a class:
public class Employee {
private String name;
private String department;
public Employee(String name, String department) {
this.name = name;
this.department = department;
}
public String getName() { return name; }
public String getDepartment() { return department; }
}
Build a map “name → department”:
List<Employee> employees = List.of(
new Employee("Alice", "IT"),
new Employee("Bob", "HR"),
new Employee("Charlie", "IT")
);
Map<String, String> nameToDept = employees.stream()
.collect(Collectors.toMap(
Employee::getName,
Employee::getDepartment,
(oldValue, newValue) -> newValue // handling duplicate names
));
System.out.println(nameToDept); // {Alice=IT, Bob=HR, Charlie=IT}
Example 5: Collect unique elements into a Set
Set<String> unique = names.stream()
.collect(Collectors.toSet());
System.out.println(unique);
Example 6: Collect statistics over numbers
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
IntSummaryStatistics stats = numbers.stream()
.collect(Collectors.summarizingInt(Integer::intValue));
System.out.println(stats.getSum()); // 15
System.out.println(stats.getAverage()); // 3.0
System.out.println(stats.getMax()); // 5
System.out.println(stats.getMin()); // 1
3. Comparison: when to use reduce, and when — collect
reduce — when you need a single final value via a binary operation: sum, product, max, concatenation.
collect — when you need to gather elements into a collection/map/string or perform a more complex aggregation with a Collector. For such tasks, collect is usually more powerful and efficient.
Table: reduce vs. collect
| Task | What to use | Example |
|---|---|---|
| Sum of numbers | |
|
| Product | |
|
| Collect into List | |
|
| Collect into Map | |
|
| Grouping | |
|
| String concatenation | reduce / collect | reduce("", String::concat) or Collectors.joining() |
4. Practical tasks
Task 1: Find the sum of lengths of all strings in a list
List<String> words = List.of("cat", "dog", "elephant");
int totalLength = words.stream()
.mapToInt(String::length)
.sum(); // or via reduce: .reduce(0, Integer::sum)
System.out.println(totalLength); // 14
Task 2: Collect unique elements into a Set
List<String> fruits = List.of("apple", "pear", "apple", "orange");
Set<String> uniqueFruits = fruits.stream()
.collect(Collectors.toSet());
System.out.println(uniqueFruits); // [apple, pear, orange]
Task 3: Build a Map from a list of objects
List<Employee> employees = List.of(
new Employee("Alice", "IT"),
new Employee("Bob", "HR"),
new Employee("Charlie", "IT")
);
Map<String, String> nameToDept = employees.stream()
.collect(Collectors.toMap(
Employee::getName,
Employee::getDepartment,
(oldValue, newValue) -> newValue // if names are the same
));
System.out.println(nameToDept);
Task 4: Join all names with a comma
String allNames = employees.stream()
.map(Employee::getName)
.collect(Collectors.joining(", "));
System.out.println(allNames); // Alice, Bob, Charlie
5. Implementation details and nuances
Optional and reduce
If you use reduce without an identity, the result is an Optional. This is safe: if the stream is empty, the result is empty too. Don’t forget to handle it properly: ifPresent(...), orElse(...), orElseThrow(...).
Optional<Integer> max = numbers.stream().reduce(Integer::max);
max.ifPresent(System.out::println);
Custom collectors: if you want to go extreme
You can write your own Collector if the standard ones are not enough. But for 99% of tasks, the ready-made ones in Collectors are sufficient.
Collectors and parallel streams
The ready-made collectors from Collectors are designed to work correctly with parallelStream(). Don’t manually add elements to a shared mutable collection inside forEach on a parallel stream — you’ll hit data races.
6. Common mistakes when working with reduce and collect
Error No. 1: You don’t check Optional after reduce. If the stream is empty, reduce without an identity returns an empty Optional. Calling get() will cause a NoSuchElementException. Use ifPresent, orElse, or orElseThrow.
Error No. 2: You try to build a collection via reduce. It’s possible, but collect is designed for this and is faster:
// Inefficient!
List<String> list = stream.reduce(
new ArrayList<>(),
(acc, elem) -> { acc.add(elem); return acc; },
(acc1, acc2) -> { acc1.addAll(acc2); return acc1; }
);
// Better:
List<String> list2 = stream.collect(Collectors.toList());
Error No. 3: You don’t handle duplicate keys in toMap. If keys collide, you’ll get an exception. Add the third argument to toMap to resolve conflicts.
Error No. 4: You use mutable collections in parallel streams without synchronization. In collect, use standard collectors — they work correctly in parallel mode. Don’t do list.add() inside forEach on a parallelStream().
Error No. 5: You confuse reduce and collect for complex tasks. reduce — for simple aggregations (sum, max). collect — for collecting into collections, grouping, building a Map, and complex aggregation.
GO TO FULL VERSION