Java Stream reduce vs collect: Key Differences
java stream reduce vs collect: Understand the differences between Java Stream reduce and collect, and learn when to use each terminal operation for efficient data proc...
The Decision Between reduce and collect
When working with Java streams, both reduce and collect are terminal operations that produce a result from the elements of a stream. They often appear interchangeable at first glance, but they are designed for different kinds of reductions. The choice between java stream reduce vs collect affects code clarity, performance, and how easily the operation can be parallelized.
reduce performs an immutable reduction: it combines stream elements into a single value using a binary operator, without modifying any external state. collect performs a mutable reduction: it accumulates elements into a mutable container, such as a List, Map, or StringBuilder, and then optionally transforms that container.
Understanding this distinction is the first step in deciding which operation fits your use case.
How reduce Works
The reduce method comes in three overloads. The simplest takes a BinaryOperator<T> and returns an Optional<T>:
List<Integer> numbers = List.of(1, 2, 3, 4, 5); Optional<Integer> sum = numbers.stream().reduce(Integer::sum);
The second overload takes an identity value and a BinaryOperator<T>:
int sum = numbers.stream().reduce(0, Integer::sum);
The third overload is used with parallel streams and takes an identity, a BiFunction for accumulating, and a BinaryOperator for combining partial results. This overload is rarely needed in sequential streams but is essential for parallel reduction.
Because reduce works with immutable values, each step creates a new result. This is safe for parallel execution because partial results can be combined independently. However, using reduce to build a collection is inefficient because it would copy the collection at every step.
How collect Works
The collect method is designed for mutable reduction. It takes a Collector that defines how to create a mutable container, how to add each element to it, and how to combine two containers in parallel.
The most common collectors are provided by the Collectors utility class:
List<String> names = people.stream() .map(Person::getName) .collect(Collectors.toList()); Map<String, List<Person>> byCity = people.stream() .collect(Collectors.groupingBy(Person::getCity)); String joined = names.stream().collect(Collectors.joining(", "));
collect is the right choice when the result is a collection or a complex object that can be built incrementally. The mutable container is updated in place, avoiding the repeated copying that would occur with reduce.
When to Use reduce
Use reduce when you want to produce a single immutable value from a stream, such as a sum, product, maximum, or minimum. The operation is naturally associative, which is a requirement for parallel reduction.
int product = numbers.stream().reduce(1, (a, b) -> a * b);
You can also use reduce to implement custom aggregation logic that does not have a corresponding collector, as long as the operation is associative and the identity is correct.
Avoid using reduce to build collections or mutable objects. The following code is a common anti-pattern:
List<String> result = stream.reduce(new ArrayList<>(), (list, element) -> { list.add(element); return list; }, (list1, list2) -> { list1.addAll(list2); return list1; });
This works but is unnecessarily complex and may cause issues with parallel streams because it mutates the same list in multiple threads. collect is the intended tool for this job.
When to Use collect
Use collect whenever the result is a mutable container or a value that can be built by accumulating elements. This includes lists, sets, maps, strings, and custom objects.
collect is also more flexible because collectors can be composed. For example, you can use Collectors.groupingBy with a downstream collector to produce a nested structure:
Map<String, Long> countByCity = people.stream() .collect(Collectors.groupingBy(Person::getCity, Collectors.counting()));
The Collectors class provides many ready-made collectors, and you can create custom collectors with Collector.of when you need specific behavior.
Performance and Parallelism Considerations
Both reduce and collect can be parallelized, but they have different characteristics.
reduce is inherently parallel-friendly because it does not mutate shared state. The stream can split the input, compute partial results in parallel, and combine them with the combiner function. This works well for numeric operations and other associative functions.
collect also supports parallelism, but it requires that the collector's combiner function correctly merges two containers. Many built-in collectors are designed for this, but custom collectors must ensure thread safety and proper merging.
In sequential streams, the performance difference is usually negligible. The choice should be driven by the type of result you need, not by micro-optimizations. However, using reduce to build a collection will be slower than collect because of the repeated copying.
Common Pitfalls
One common mistake is using reduce with a non-associative function. For example, subtraction is not associative, so reduce may produce different results in parallel streams.
Another pitfall is using a mutable object as the identity value in reduce. The identity must be an actual identity element for the binary operator, and it should not be modified during the reduction.
For collect, a frequent issue is using a collector that is not CONCURRENT in a parallel stream without a combiner. Most collectors handle this correctly, but custom collectors need to be designed carefully.
Custom Collectors and Advanced Usage
When the built-in collectors are not enough, you can create a custom collector using Collector.of. This allows you to define the supplier, accumulator, combiner, and finisher.
Collector<Person, StringBuilder, String> nameCollector = Collector.of( StringBuilder::new, (sb, person) -> sb.append(person.getName()).append(", "), StringBuilder::append, sb -> sb.toString() );
This gives you full control over the reduction process and is often clearer than a complex reduce chain.
In summary, reduce is for immutable reduction, collect is for mutable reduction. Choose the one that matches the shape of your result and the semantics of your operation.