Introduction
When collecting a stream, the groupingBy()
Collector(java.util.stream.Collector
) can be used to put data into different groups based on specified conditions. groupingBy()
is a little bit more complicated to use than partitioningBy() because the key can be of any type, whereas with partitioningBy()
, the keys must be of type Boolean.
There are 3 overloaded variants of the groupingBy()
method in the Collectors(java.util.stream.Collectors
) class. In this tutorial, we are going to learn how to use all of these 3 variants of groupingBy()
.
Goals
At the end of the tutorial, you would have learned:
- How to use the
groupingBy()
Collector.
Prerequisite Knowledge
- Intermediate Java.
- Java Streams(
java.util.stream.Stream
).
Tools Required
- A Java IDE.
Project Setup
To follow along with the tutorial, perform the steps below:
-
Create a new Java project.
-
Create a package
com.example
. -
Create a class called
Entry
. -
Create the
main()
method insideEntry.java
. -
Our examples will use Cake objects with different properties to demonstrate
groupingBy()
, so declare the package-private top-level record class Cake insideEntry.java
from the code below.record Cake(Shape shape, Color color, BigDecimal price){}
-
Then create an enum called
Shape
.enum Shape { TRIANGLE, CIRCLE, DIAMOND }
-
We do not have to create our own class to encapsulate colors, we will use the premade Java class
java.awt.Color
.
groupingBy()
Concept Overview
In main()
, add the List of cakes using the code below:
var cakes = List.of(
new Cake(Shape.TRIANGLE, Color.BLUE, BigDecimal.valueOf(4.99)),
new Cake(Shape.CIRCLE, Color.RED, BigDecimal.valueOf(3.99)),
new Cake(Shape.DIAMOND, Color.CYAN, BigDecimal.valueOf(5.25)),
new Cake(Shape.CIRCLE, Color.GREEN, BigDecimal.valueOf(3.49)),
new Cake(Shape.DIAMOND, Color.BLACK, BigDecimal.valueOf(5.99))
);
Whenever we collect a stream using groupingBy()
, we have to tell groupingBy()
the key that we want to group items by.
For example, if we wanted to group the Cakes by Shape
alone, we will end up with a Map with 3 different keys, and the keys being the Shapes themselves. The picture below simplifies the data structure returned by groupingBy()
.
We do not necessarily tell groupingBy()
"how" to group things, instead, we mostly want to tell it “what” to group things by. In the List of Cakes above, we can tell groupingBy()
to group Cakes by their Shape, Color, or price. It is not that we are not allowed to tell it how to group things by, but if we are performing comparisons on the items themselves, then we might as well use partitioningBy()
.
The differences between groupingBy()
and partitioningBy()
are:
- The Map returned by
partitioningBy()
will always produce only two Boolean keys, true and false, whereas the Map returned bygroupingBy()
can have keys of any type and can have as many keys as you want. - There is no version of
partitioningBy()
that allows you to specify a custom Map implementation of your choice, but there is a version ofgroupingBy()
that allows you to do so.
Simple groupingBy()
Variant
Let us start with the simplest groupingBy()
variant, where we only have to provide one argument. Its method signature is:
static <T, K> Collector<T,?,Map<K,List<T>>> groupingBy(Function<? super T,? extends K> classifier)
The only argument that we will have to provide is a Function
object. It does not have to be anything fancy. A Function that extracts a property of each Cake will do. groupingBy()
will automatically group the Cakes with the same property value for us.
In the Entry
class, create a new method called groupByShape()
from the code below:
private static void groupByShape(List<Cake> cakes){
Map<Shape, List<Cake>> cakeGroups = cakes.stream()
.collect(Collectors.groupingBy(Cake::shape)
);
cakeGroups.entrySet().forEach(System.out::println);
}
And then call it in main()
with
groupByShape(cakes);
The output would be:
DIAMOND=[Cake[shape=DIAMOND, color=java.awt.Color[r=0,g=255,b=255], price=5.25], Cake[shape=DIAMOND, color=java.awt.Color[r=0,g=0,b=0], price=5.99]]
TRIANGLE=[Cake[shape=TRIANGLE, color=java.awt.Color[r=0,g=0,b=255], price=4.99]]
CIRCLE=[Cake[shape=CIRCLE, color=java.awt.Color[r=255,g=0,b=0], price=3.99], Cake[shape=CIRCLE, color=java.awt.Color[r=0,g=255,b=0], price=3.49]]
The groupByShape()
method obviously groups Cakes by their Shape, and that is why our Map has 3 keys, with each being a Shape. Our List<Cake>
only has one Cake with the TRIANGLE
Shape, so we only see one Cake for the key TRIANGLE
. For the other two Shapes, our List<Cake>
has two of each, so that is why there are two Cakes for each Shape.
groupingBy()
with a downstream Collector
The second variant of groupingBy()
allows you to pass additional Collectors into it. If you want to group the Cakes by Shape and by Color as well, then this is the correct variant to use. Its method signature is:
static <T, K, A, D> Collector<T,?,Map<K,D>> groupingBy(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream)
Add another method called groupByShapeThenColor()
in the Entry
class as well to see how the method is used:
private static void groupByShapeThenColor(List<Cake> cakes){
Map<Shape, Map<Color, List<Cake>>> cakeGroups;
cakeGroups = cakes.stream().collect(Collectors.groupingBy(
Cake::shape,
Collectors.groupingBy(Cake::color)
));
cakeGroups.entrySet().forEach(System.out::println);
}
When calling it in main()
, the output is:
CIRCLE={java.awt.Color[r=0,g=255,b=0]=[Cake[shape=CIRCLE, color=java.awt.Color[r=0,g=255,b=0], price=3.49]], java.awt.Color[r=255,g=0,b=0]=[Cake[shape=CIRCLE, color=java.awt.Color[r=255,g=0,b=0], price=3.99]]}
TRIANGLE={java.awt.Color[r=0,g=0,b=255]=[Cake[shape=TRIANGLE, color=java.awt.Color[r=0,g=0,b=255], price=4.99]]}
DIAMOND={java.awt.Color[r=0,g=0,b=0]=[Cake[shape=DIAMOND, color=java.awt.Color[r=0,g=0,b=0], price=5.99]], java.awt.Color[r=0,g=255,b=255]=[Cake[shape=DIAMOND, color=java.awt.Color[r=0,g=255,b=255], price=5.25]]}
Now we have nested Maps where the top-level keys are Shapes and the secondary-level keys are the Colors. The Cakes are first grouped by Shapes, and within each group, there will be subgroups by Color.
groupingBy()
with custom Map implementation
All of the groupingBy()
Collectors that we have used so far returned a Map, but did you notice that we were not able to specify a specific implementation of Map(HashMap, TreeMap) that we want? Fortunately, the last variant of groupingBy()
allows us to do just that. It requires a Supplier that will supply a specific instance of Map. Its method signature is:
static <T, K, D, A, M extends Map<K, D>> Collector<T,?,M> groupingBy(Function<? super T,? extends K> classifier, Supplier<M> mapFactory, Collector<? super T,A,D> downstream)
To see how it is used, add a new method called groupByShapeThenColorCustomMap()
into the Entry
class.
private static void groupByShapeThenColorCustomMap(List<Cake> cakes){
var cakeGroups = cakes.stream().collect(Collectors.groupingBy(
Cake::shape,
TreeMap::new,
Collectors.groupingBy(Cake::color)
));
cakeGroups.entrySet().forEach(System.out::println);
}
Notice that in the second parameter, we have specified that we want an instance of TreeMap, which will automatically sort the keys for us.
When we call it in main()
, the output is:
TRIANGLE={java.awt.Color[r=0,g=0,b=255]=[Cake[shape=TRIANGLE, color=java.awt.Color[r=0,g=0,b=255], price=4.99]]}
CIRCLE={java.awt.Color[r=0,g=255,b=0]=[Cake[shape=CIRCLE, color=java.awt.Color[r=0,g=255,b=0], price=3.49]], java.awt.Color[r=255,g=0,b=0]=[Cake[shape=CIRCLE, color=java.awt.Color[r=255,g=0,b=0], price=3.99]]}
DIAMOND={java.awt.Color[r=0,g=0,b=0]=[Cake[shape=DIAMOND, color=java.awt.Color[r=0,g=0,b=0], price=5.99]], java.awt.Color[r=0,g=255,b=255]=[Cake[shape=DIAMOND, color=java.awt.Color[r=0,g=255,b=255], price=5.25]]}
enum constants are sorted by the order of their declaration, so that is why the keys are sorted in this order.
Solution Code
package com.example;
import java.awt.*;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class Entry {
public static void main(String[] args){
var cakes = List.of(
new Cake(Shape.TRIANGLE, Color.BLUE, BigDecimal.valueOf(4.99)),
new Cake(Shape.CIRCLE, Color.RED, BigDecimal.valueOf(3.99)),
new Cake(Shape.DIAMOND, Color.CYAN, BigDecimal.valueOf(5.25)),
new Cake(Shape.CIRCLE, Color.GREEN, BigDecimal.valueOf(3.49)),
new Cake(Shape.DIAMOND, Color.BLACK, BigDecimal.valueOf(5.99))
);
//groupByShape(cakes);
//groupByShapeThenColor(cakes);
groupByShapeThenColorCustomMap(cakes);
}
private static void groupByShape(List<Cake> cakes){
Map<Shape, List<Cake>> cakeGroups = cakes.stream()
.collect(Collectors.groupingBy(Cake::shape)
);
cakeGroups.entrySet().forEach(System.out::println);
}
private static void groupByShapeThenColor(List<Cake> cakes){
Map<Shape, Map<Color, List<Cake>>> cakeGroups;
cakeGroups = cakes.stream().collect(Collectors.groupingBy(
Cake::shape,
Collectors.groupingBy(Cake::color)
));
cakeGroups.entrySet().forEach(System.out::println);
}
private static void groupByShapeThenColorCustomMap(List<Cake> cakes){
var cakeGroups = cakes.stream().collect(Collectors.groupingBy(
Cake::shape,
TreeMap::new,
Collectors.groupingBy(Cake::color)
));
cakeGroups.entrySet().forEach(System.out::println);
}
}
record Cake(Shape shape, Color color, BigDecimal price){}
enum Shape { TRIANGLE, CIRCLE, DIAMOND }
Summary
We have learned how to use the groupingBy()
Collector in this tutorial, the full project code can be found here https://github.com/dmitrilc/DaniwebJavaGroupingBy/tree/master