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:
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:
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:
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:
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.
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:
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.