Stream Reduction Operations

In this guide, we will discuss general purpose reduction operations which are used using Collections
The JDK contains many terminal operations (such as average, sum, min, max, and count) that return one value by combining the contents of a stream. These operations are called reduction operations. 
Examples:
average
sum
min
max
count
The JDK also contains reduction operations that return a collection instead of a single value. Many reduction operations perform a specific task, such as finding the average of values or grouping elements into categories.

In this post, we will discuss below two methods.
  1. The Stream.reduce Method
  2. The Stream.collect Method

1. The Stream.reduce Method

The Stream.reduce method is a general-purpose reduction operation.

Example: Sum of ages with sum operation

Consider the following pipeline, which calculates the sum of the male members' ages in the collection roster. It uses the Stream.sum reduction operation:
Integer totalAge = roster
    .stream()
    .mapToInt(Person::getAge)
    .sum();

Example: Sum of ages with reduce(identity, accumulator)

Compare the above example with the following pipeline, which uses the Stream.reduce operation to calculate the same value:
Integer totalAgeReduce = roster
   .stream()
   .map(Person::getAge)
   .reduce(
       0,
       (a, b) -> a + b);
The reduce operation in this example takes two arguments:
identity: The identity element is both the initial value of the reduction and the default result if there are no elements in the stream. In this example, the identity element is 0; this is the initial value of the sum of ages and the default value if no members exist in the collection roster.
accumulator: The accumulator function takes two parameters: a partial result of the reduction (in this example, the sum of all processed integers so far) and the next element of the stream (in this example, an integer). It returns a new partial result. In this example, the accumulator function is a lambda expression that adds two Integer values and returns an Integer value:
(a, b) -> a + b

Internal Working of reduce() function

Performs a reduction on the elements of a given stream, using the provided identity value and an associative accumulation function, and returns the reduced value. This is equivalent to:
T result = identity;
     for (T element : this stream)
         result = accumulator.apply(result, element)
     return result;
The identity value must be an identity for the accumulator function. This means that for all t, accumulator.apply(identity, t) is equal to t. The accumulator function must be an associative function.

2. The Stream.collect Method

Unlike the reduce method, which always creates a new value when it processes an element, the collect method modifies or mutates, an existing value.

Example: Average of male members with collect operation

Consider how to find the average of values in a stream. You require two pieces of data: the total number of values and the sum of those values. However, like the reduce method and all other reduction methods, the collect method returns only one value. You can create a new data type that contains member variables that keep track of the total number of values and the sum of those values, such as the following class, Averager:

Averager.java

class Averager implements IntConsumer
{
    private int total = 0;
    private int count = 0;
        
    public double average() {
        return count > 0 ? ((double) total)/count : 0;
    }
        
    public void accept(int i) { total += i; count++; }
    public void combine(Averager other) {
        total += other.total;
        count += other.count;
    }
}
The following pipeline uses the Averager class and the collect method to calculate the average age of all male members:
Averager averageCollect = roster.stream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .map(Person::getAge)
    .collect(Averager::new, Averager::accept, Averager::combine);
                   
System.out.println("Average age of male members: " +
    averageCollect.average());
The collect operation in this example takes three arguments:
supplier: The supplier is a factory function; it constructs new instances. For the collect operation, it creates instances of the resulting container. In this example, it is a new instance of the Averager class.
accumulator: The accumulator function incorporates a stream element into a result container. In this example, it modifies the Averager result container by incrementing the count variable by one and adding to the total member variable the value of the stream element, which is an integer representing the age of a male member.
combiner: The combiner function takes two result containers and merges their contents. In this example, it modifies an Averager result container by incrementing the count variable by the count member variable of the other Averager instance and adding to the total member variable the value of the other Averager instance's total member variable.

Example: Names of male members with collect operation

The collect operation is best suited for collections. The following example puts the names of the male members in a collection with the collect operation:
List<String> namesOfMaleMembersCollect = roster
    .stream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .map(p -> p.getName())
    .collect(Collectors.toList());
This version of the collect operation takes one parameter of type Collector. This class encapsulates the functions used as arguments in the collect operation that requires three arguments (supplier, accumulator, and combiner functions).
The Collectors class contains many useful reduction operations, such as accumulating elements into collections and summarizing elements according to various criteria. These reduction operations return instances of the class Collector, so you can use them as a parameter for the collect operation.
This example uses the Collectors.toList operation, which accumulates the stream elements into a new instance of List. As with most operations in the Collectors class, the toList operator returns an instance of Collector, not a collection.

Example: Group members by gender

The following example groups members of the collection roster by gender:
Map<Person.Sex, List<Person>> byGender =
    roster
        .stream()
        .collect(
            Collectors.groupingBy(Person::getGender));
The groupingBy operation returns a map whose keys are the values that result from applying the lambda expression specified as its parameter (which is called a classification function).

Example: Group names by gender

The following example retrieves the names of each member in the collection roster and groups them by gender:
Map<Person.Sex, List<String>> namesByGender =
    roster
        .stream()
        .collect(
            Collectors.groupingBy(
                Person::getGender,                      
                Collectors.mapping(
                    Person::getName,
                    Collectors.toList())));
The groupingBy operation in this example takes two parameters, a classification function and an instance of Collector. The Collector parameter is called a downstream collector.

Example: Total age by gender

The following example retrieves the total age of members of each gender:
Map<Person.Sex, Integer> totalAgeByGender =
    roster
        .stream()
        .collect(
            Collectors.groupingBy(
                Person::getGender,                      
                Collectors.reducing(
                    0,
                    Person::getAge,
                    Integer::sum)));
The reducing operation takes three parameters:
identity: Like the Stream.reduce operation, the identity element is both the initial value of the reduction and the default result if there are no elements in the stream. In this example, the identity element is 0; this is the initial value of the sum of ages and the default value if no members exist.
mapper: The reducing operation applies this mapper function to all stream elements. In this example, the mapper retrieves the age of each member.
operation: The operation function is used to reduce the mapped values. In this example, the operation function adds Integer values.

Example: Average age by gender

The following example retrieves the average age of members of each gender:
Map<Person.Sex, Double> averageAgeByGender = roster
    .stream()
    .collect(
        Collectors.groupingBy(
            Person::getGender,                      
            Collectors.averagingInt(Person::getAge)));
for (Map.Entry<Person.Sex, Double> e : averageAgeByGender.entrySet()) {
            System.out.println(e.getKey() + ": " + e.getValue());
}

Complete Code for Reference

package com.javaguides.collections.aggregateoperations;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
 
/**
 * Stream api reduction examples
 * @author javaguides.net
 *
 */
public class ReductionExamples {
 
    public static void main(String... args) {
         
        // Create sample data
 
        List<Person> roster = Person.createRoster();
     
        System.out.println("Contents of roster:");
         
        roster
            .stream()
            .forEach(p -> p.printPerson());
 
        System.out.println();
 
        // 1. Average age of male members, average operation
         
        double average = roster
            .stream()
            .filter(p -> p.getGender() == Person.Sex.MALE)
            .mapToInt(Person::getAge)
            .average()
            .getAsDouble();
             
        System.out.println("Average age (bulk data operations): " +
            average);
         
        // 2. Sum of ages with sum operation
         
        Integer totalAge = roster
            .stream()
            .mapToInt(Person::getAge)
            .sum();
             
        System.out.println("Sum of ages (sum operation): " +
            totalAge);
         
        // 3. Sum of ages with reduce(identity, accumulator)
         
        Integer totalAgeReduce = roster
            .stream()
            .map(Person::getAge)
            .reduce(
                0,
                (a, b) -> a + b);
             
        System.out.println(
            "Sum of ages with reduce(identity, accumulator): " +
            totalAgeReduce);
          
      
        // 5. Names of male members with collect operation
 
        System.out.println("Names of male members with collect operation: ");         
        List<String> namesOfMaleMembersCollect = roster
            .stream()
            .filter(p -> p.getGender() == Person.Sex.MALE)
            .map(p -> p.getName())
            .collect(Collectors.toList());         
 
        namesOfMaleMembersCollect
            .stream()
            .forEach(p -> System.out.println(p));
              
        // 6. Group members by gender
          
        System.out.println("Members by gender:");
        Map<Person.Sex, List<Person>> byGender =
            roster
                .stream()
                .collect(
                    Collectors.groupingBy(Person::getGender));
                 
        List<Map.Entry<Person.Sex, List<Person>>>
            byGenderList = 
            new ArrayList<>(byGender.entrySet());
             
        byGenderList
            .stream()
            .forEach(e -> {
                System.out.println("Gender: " + e.getKey());
                e.getValue()
                    .stream()
                    .map(Person::getName)
                    .forEach(f -> System.out.println(f)); });
          
        // 7. Group names by gender
          
        System.out.println("Names by gender:");
        Map<Person.Sex, List<String>> namesByGender =
            roster
                .stream()
                .collect(
                     Collectors.groupingBy(
                         Person::getGender,                      
                         Collectors.mapping(
                             Person::getName,
                             Collectors.toList())));
                      
        List<Map.Entry<Person.Sex, List<String>>>
            namesByGenderList = 
                new ArrayList<>(namesByGender.entrySet());
                      
        namesByGenderList
            .stream()
            .forEach(e -> {
                System.out.println("Gender: " + e.getKey());
                e.getValue()
                    .stream()
                    .forEach(f -> System.out.println(f)); });
          
        // 8. Total age by gender
         
        System.out.println("Total age by gender:");
        Map<Person.Sex, Integer> totalAgeByGender =
            roster
                .stream()
                .collect(
                     Collectors.groupingBy(
                         Person::getGender,                      
                         Collectors.reducing(
                             0,
                             Person::getAge,
                             Integer::sum)));
                 
        List<Map.Entry<Person.Sex, Integer>>
            totalAgeByGenderList = 
            new ArrayList<>(totalAgeByGender.entrySet());
                      
        totalAgeByGenderList
            .stream()
            .forEach(e -> 
                System.out.println("Gender: " + e.getKey() +
                    ", Total Age: " + e.getValue()));
              
        // 9. Average age by gender
          
        System.out.println("Average age by gender:");
        Map<Person.Sex, Double> averageAgeByGender =
            roster
                .stream()
                .collect(
                     Collectors.groupingBy(
                         Person::getGender,                      
                         Collectors.averagingInt(Person::getAge)));
                  
        for (Map.Entry<Person.Sex, Double> e : averageAgeByGender.entrySet()) {
            System.out.println(e.getKey() + ": " + e.getValue());
        }
    }
}
Output;
Contents of roster:
Fred, 38
Jane, 28
George, 26
Bob, 17

Average age (bulk data operations): 27.0
Sum of ages (sum operation): 109
Sum of ages with reduce(identity, accumulator): 109
Names of male members with collect operation: 
Fred
George
Bob
Members by gender:
Gender: MALE
Fred
George
Bob
Gender: FEMALE
Jane
Names by gender:
Gender: MALE
Fred
George
Bob
Gender: FEMALE
Jane
Total age by gender:
Gender: MALE, Total Age: 81
Gender: FEMALE, Total Age: 28
Average age by gender:
MALE: 27.0
FEMALE: 28.0


Comments