CodeGym /Courses /JAVA 25 SELF /Reduce and collect methods: data aggregation

Reduce and collect methods: data aggregation

JAVA 25 SELF
Level 31 , Lesson 1
Available

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
reduce
reduce(0, Integer::sum)
Product
reduce
reduce(1, (a, b) -> a * b)
Collect into List
collect
collect(Collectors.toList())
Collect into Map
collect
collect(Collectors.toMap(...))
Grouping
collect
collect(Collectors.groupingBy(...))
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.

1
Task
JAVA 25 SELF, level 31, lesson 1
Locked
Finding the peak temperature for the week 🌡️
Finding the peak temperature for the week 🌡️
1
Task
JAVA 25 SELF, level 31, lesson 1
Locked
Creating a Magic Spell 📜
Creating a Magic Spell 📜
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION