Grouping & Partitioning StreamsJ8 Home « Grouping & Partitioning Streams

In this second lesson on using stream collectors that use the overloaded collect() method of the Stream<E> interface, we look at grouping and partitioning our streams.

Collecting and aggregatings our streams is discussed in the Collecting & Aggregating Streams lesson.

So we have our streams and we want to collate the elements into something meaningful groupwise, what are the options? There are three variants of the groupingBy static method, in the Collectors class designed just for this purpose:

  • one parameter groupingBy static method that takes a classification function as input and can be used for single-level grouping.
  • two parameter groupingBy static method that takes a classification function and collector as input and can be used for multi-level grouping.
  • three parameter groupingBy static method that takes a classification function, map factory and collector as input and can be used for multi-level grouping to the mapping specified.

Grouping Top

Lets take a look at some of the static methods of the Collectors class used for grouping our streams.

Following is a TestGrouping class to demonstrate some usage:


package info.java8;

import java.util.*;
import java.util.stream.Collectors;

import static java.util.stream.Collectors.*;

/* Grouping streams */
public class TestGrouping {

    /* Grouping streams */
    public static void main(String[] args) {

        // Group by sex
        Map<Employee.Sex, List<Employee>> sexEmployee = Employee.listOfStaff().stream()
                .collect(groupingBy(Employee::getGender));
        sexEmployee.forEach((k,v) -> System.out.println(k + ":" + v));

        // Group by age
        Map<String, List<Employee>> ageEmployee = Employee.listOfStaff().stream()
                .collect(groupingBy(
                        employee -> {
                            if (employee.getAge() <25) {
                                return "younger";
                            } else if (employee.getAge() >50) {
                                return "older";
                            } else {
                                return "middler";
                            }
                        }));
        ageEmployee.forEach((k,v) -> System.out.println(k + ":" + v));

        /* Group by roles declaratively */
        Map<String, List<String>> roleAndNames = Employee.listOfStaff().stream()
                .flatMap(employee -> employee.getRoles()
                        .stream()
                        .map(role -> new AbstractMap.SimpleEntry<>(role, employee.getName())))
                .collect(Collectors.groupingBy(
                        Map.Entry::getKey,
                        Collectors.mapping(Map.Entry::getValue, Collectors.toList())));

        roleAndNames.forEach((k,v) -> System.out.println(k + ":" + v));

        /* Group by roles iteratively */
        Map<String, List<String>> roleAndNames2 = new HashMap<>();
        for (Employee employee: Employee.listOfStaff()) {
            for (String role: employee.getRoles()) {
                roleAndNames2.computeIfAbsent(role, roleKey -> new ArrayList<>())
                        .add(employee.getName());
            }
        }
        roleAndNames2.forEach((k,v) -> System.out.println(k + ":" + v));
    }
}

Building and running the TestGrouping class produces the following output:

Run TestGrouping class
Screenshot 1. Running the TestGrouping class.

That's quite a lot of code to go through, so lets see what's new!

We used collect() in conjunction with the one parameter groupingBy() static method of the Collectors class which takes a classifier and returns a Collector implementing a group by operation on input elements of type T, grouping elements according to a classification function, and returning the results in a Map. In our example we group by sex using a method reference for employees within each classification.

In the second example we use collect() in conjunction with the one parameter groupingBy() static method to group by age using a lambda rather than a method reference, there won't always be a method reference!

The third example is a bit more complicated, what we are doing is first exploding the role (which is a list) using flatMap() to get individual values and then mapping a role and name i.e. receptionist=Dorothy. We use these group/pairs as entry to our groupingBy() static method to group by names within roles.

The fourth example is the same as the third using iterative code. In this case the code is actually less verbose than the declarative code and points to the fact that you should use streams judiciously and in various circumstances an iterative based approach is more suitable.

Multilevel Grouping Top

Multi level grouping is achieved by using the two parameter groupingBy() static method of the Collectors class which takes a classifier and a collector and returns a Collector implementing a cascaded group by operation on input elements of type T, grouping elements according to a classification function, and then performing a reduction operation on the values associated with a given key using the specified downstream Collector.

Lets take a look at some code that does some multilevel grouping.

Following is a TestMultiLevelGroupingA class to demonstrate some usage:


package info.java8;

import java.util.List;
import java.util.Map;

import static java.util.stream.Collectors.groupingBy;

/* Grouping streams */
public class TestMultiLevelGroupingA {

    /* Grouping streams */
    public static void main(String[] args) {

        // Group by age within sex
        Map<Employee.Sex, Map<Object, List<Employee>>> ageEmployee = Employee.listOfStaff().stream()
                .collect(
                        groupingBy(Employee::getGender,
                                groupingBy(employee -> {
                                    if (employee.getAge() <25) {
                                        return "younger";
                                    } else if (employee.getAge() >50) {
                                        return "older";
                                    } else {
                                        return "middler";
                                    }
                                })));
        ageEmployee.forEach((k,v) -> System.out.println(k + ":" + v));
    }
}

Building and running the TestMultiLevelGroupingA class produces the following output:

Run TestMultiLevelGroupingA class
Screenshot 2. Running the TestMultiLevelGroupingA class.

Hopefully the indenting makes the code easier to read. We are selecting a sex via the classification (first) parameter of the call to groupingBy() and then for the collector (second) parameter we use another groupingBy() with a lambda to group age, giving us sex subdivided by age and then employees.

Just to note that for any multilevel grouping we will always end up with a map within a map.

Because the two parameter groupingBy() static method of the Collectors class takes a Collector as a second parameter we can use any method that returns a collector from the Collectors class. Lets see a few more examples.

Lets look at a few more examples of multilevel grouping.

Following is a TestMultiLevelGroupingB class for this purpose:


package info.java8;

import java.util.Comparator;
import java.util.Map;
import java.util.Optional;

import static java.util.stream.Collectors.*;

/* Grouping streams */
public class TestMultiLevelGroupingB {

    /* Grouping streams */
    public static void main(String[] args) {

        // Get lowest earner of each sex
        Map<Employee.Sex, Optional<Employee>> ageEmployee = Employee.listOfStaff().stream()
                .collect(groupingBy(Employee::getGender,
                        minBy(Comparator.comparingDouble(Employee::getSalary))));
        ageEmployee.forEach((k,v) -> System.out.println(k + ":" + v));

        // Get lowest earner of each sex and remove Optional
        Map<Employee.Sex, Object> ageEmployee2 = Employee.listOfStaff().stream()
                .collect(groupingBy(Employee::getGender,
                        collectingAndThen(
                                minBy(Comparator.comparingDouble(Employee::getSalary)),
                                        Optional::get)));
        ageEmployee2.forEach((k,v) -> System.out.println(k + ":" + v));

        // Get gender count
        Map<Employee.Sex, Long> countGender = Employee.listOfStaff().stream()
                .collect(groupingBy(Employee::getGender,
                        counting()));
        countGender.forEach((k,v) -> System.out.println(k + ":" + v));
    }
}

Building and running the TestMultiLevelGroupingB class produces the following output:

Run TestMultiLevelGroupingB class
Screenshot 3. Running the TestMultiLevelGroupingB class.

In the first example we are selecting a sex via the classification (first) parameter of the call to groupingBy() and then for the collector (second) parameter we use the minBy() method to get the lowest earner for each sex.

The second example is similar to the first but we make the output more readable by removing the Optional. We do this by using the collectingAndThen static method of the Collectors class which adapts a Collector to perform an additional finishing transformation. In this case we use the minBy() method to get the lowest earner for each sex and then use the get method of the Optional class to transform to the optional value and return this.

Mapping Our Groupings Top

There is also a three parameter groupingBy() static method of the Collectors class which takes a classifier, map factory and collector and returns a Collector implementing a cascaded group by operation on input elements of type T, grouping elements according to a classification function, and then performing a reduction operation on the values associated with a given key using the specified downstream Collector. The Map produced by the Collector is created with the supplied factory function.

Lets take a look at some code that maps our groupings.

Following is a TestMappingGrouping class to demonstrate some usage:


package info.java8;

import java.util.*;

import static java.util.stream.Collectors.*;

/* Grouping streams */
public class TestMappingGrouping {

    /* Grouping & mapping streams */
    public static void main(String[] args) {

        /* Group by roles within sorted name */
        Map<String, Set<List<String>>> rolesByName = Employee.listOfStaff().stream()
                .collect(groupingBy(Employee::getName, TreeMap::new,
                        mapping(Employee::getRoles, toSet())));
        rolesByName.forEach((k,v) -> System.out.println(k + ":" + v));

    }
}

Building and running the TestMultiLevelGroupingA class produces the following output:

Run TestMappingGrouping class
Screenshot 4. Running the TestMappingGrouping class.

In the above example we are selecting a name via the classification (first) parameter of the call to groupingBy() and then for the map factory (second) parameter we instantiate a TreeMap which our grouping will be sent to and then for the collector (third) we store each List of roles in a Set. We end up with a TreeMap sorted by name within which we have a Set containing a List of roles.

Partitioning Top

Partitioning is similar to grouping but the classifier function used is a predicate returning a boolean, so what does this mean? Well the resultant map produced from the collect() method will never have more than two groups, it will have been partitioned into groups for false and groups for true and so the two keys unsurprisingly, will be false and true. We achieve this using the partitioningBy() static method of the Collectors class.

There are two variants of the groupingBy static method:

  • one parameter partitioningBy static method that takes a predicate function as input and can be used for single-level grouping.
  • two parameter partitioningBy static method that takes a predicate function and collector as input and can be used for multi-level grouping.

Lets take a look at the static methods of the Collectors class used for partitioning our streams.

Following is a TestPartitioning class to demonstrate some usage:


package info.java8;

import java.util.*;

import static java.util.stream.Collectors.*;

/* Partitioning streams */
public class TestPartitioning {

    public static void main(String[] args) {

        // Partition by salary
        Map<Boolean, List<Employee>> SalaryTwentyFiveK = Employee.listOfStaff().stream()
                .collect(partitioningBy(employee -> employee.getSalary() > 24999.0));
        SalaryTwentyFiveK.forEach((k,v) -> System.out.println(k + ":" + v));
        System.out.println("\n");

        // Partition by salary and then group by employee within gender
        Map<Boolean, Map<Employee.Sex, List<Employee>>> SalaryTwentyFiveKRole = Employee.listOfStaff().stream()
                .collect(
                        partitioningBy(employee -> employee.getSalary() > 24999.0,
                                groupingBy(Employee::getGender)));
        SalaryTwentyFiveKRole.forEach((k,v) -> System.out.println(k + ":" + v));
    }
}

Building and running the TestPartitioning class produces the following output:

Run TestPartitioning class
Screenshot 5. Running the TestPartitioning class.

In the first example we use collect() in conjunction with the one parameter partitioningBy() static method of the Collectors class which takes a predicate to split employees by salary. As you can see from the screenshot our map is split into false values and true values which match the predicate.

In the second example we use collect() in conjunction with the two parameter groupingBy() static method which takes a predicate and a collector. As you can see from the screenshot our map is split into false values and true values which match the predicate and then these partitions are subgrouped by employee within gender using the groupingBy() static method.

Related Quiz

Streams Quiz 11 - Grouping & Partitioning Streams Quiz

Lesson 11 Complete

In this lesson we looked at taking our stream data and arranging it into groups and partitions.

What's Next?

In the next lesson we look at parallel streams.