Contents

Stream group by using Java

Written by: David Vlijmincx

Introduction

This post will look at some common uses of stream groupingBy. We will cover grouping by single and multiple fields, counting the elements in a group and filtering on group size. We also cover some basic statistics you can use with groupingBy.

Grouping elements in a stream

The examples in this post will use the Fruit class you can see below. The equals and hashCode methods are overridden, so two instances of the Fruit class will be equal if they have the same name. The toString method is overridden to have a more readable output in the console. The getters and setters are left out to make it more readable.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Fruit {
    String name;
    int amount;

    public Fruit(String name, int amount) {
        this.name = name;
        this.amount = amount;
    }
    
    // Getters and setters
    
    @Override
    public String toString() {return name + "Object";}

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Fruit fruit = (Fruit) o;
        return Objects.equals(name, fruit.name);
    }

    @Override
    public int hashCode() { return Objects.hash(name);}
}

This is the list of fruits we will use for the examples.

1
2
3
4
5
6
7
8
9
List<Fruit> listOfFruits = Arrays.asList(
        new Fruit("Apple", 1),
        new Fruit("Apple", 3),
        new Fruit("Banana", 2),
        new Fruit("Banana", 2),
        new Fruit("Banana", 5),
        new Fruit("Pear", 0),
        new Fruit("Orange", 4)
);

Stream group by fields

Let's start with grouping by one field. The example below will group the fruits by their name. The result is a Map with the fruit's name as key and a list of fruits as the value.

1
2
3
4
5
Map<String, List<Fruit>> collect = listOfFruits
        .stream()
        .collect(Collectors.groupingBy(
                Fruit::getName
        ));

If you print collect to the console it will show the following result. First the key (the fruits name) is printed followed by the key's value a list of fruit objects.

1
2
{Apple=[AppleObject, AppleObject], Pear=[PearObject], Orange=[OrangeObject],
 Banana=[BananaObject, BananaObject, BananaObject]}

Stream group by multiple fields

This example looks very familiar to the first example. We only added a new Collectors.groupingBy() as the second parameter to the groupingBy. The List is now first grouped by the fruit's name and then by the fruit's amount.

1
2
3
4
5
6
Map<String, Map<Integer, List<Fruit>>> collect = listOfFruits
        .stream()
        .collect(Collectors.groupingBy(
            Fruit::getName,
            Collectors.groupingBy(Fruit::getAmount)
        ));

When you print the Map the result will look like this:

1
2
{Apple={1=[AppleObject], 3=[AppleObject]}, Pear={0=[PearObject]}, Orange={4=[OrangeObject]},
 Banana={2=[BananaObject, BananaObject], 5=[BananaObject]}}

Stream group by count

We can also pass the Collectors.counting() as a parameter to the groupingBy. This collector will give you the number of objects inside the group. The result of this example is a map with the fruit's name as the key and the count as the value.

1
2
3
4
5
6
Map<String, Long> collect = listOfFruits()
        .stream()
        .collect(Collectors.groupingBy(
            fruit -> fruit.name,
            Collectors.counting()
        ));

When you print the Map, you see a group for each fruit name followed by the number of fruits in that group.

1
{Apple=2, Pear=1, Orange=1, Banana=3}

Stream group by count and filter on count

We can take the previous example further by filtering on the count. To filter on the count, we need to create a new stream of the entrySet of the map and apply the filter we want. So in the example below, we filter out all the groups that are smaller than two.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Map<String, Long> collect = listOfFruits()
        .stream()
        .collect(Collectors.groupingBy(
                Fruit::getName,
                Collectors.counting()
        ))
        .entrySet()
        .stream()
        .filter(fruitLongEntry -> fruitLongEntry.getValue() >= 2)
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

When you print the Map, we see that only apple and banana have groups size of two or more.

1
{Apple=2, Banana=3}

Stream group by sum

We can also sum all the fields in a group. To do that, you need to pass the Collectors.summingInt() to the groupingBy.

1
2
3
4
5
6
Map<Fruit, Integer> collect = listOfFruitsa
        .stream()
        .collect(Collectors.groupingBy(
                fruit -> fruit,
                Collectors.summingInt(f -> f.amount)
        ));

When you print the Map, you can see that the amount of the fruits is summed up for each group.

1
{Apple=4, Pear=0, Orange=4, Banana=9}

Stream group by and summarize (count, sum, min, avg, max)

It is also possible to get all the basic statistics when you do a groupingBy. You only have to pass the Collectors.summarizingInt as the second parameter to get these statistics. There is also a summarizingDouble and summarizingLong.

1
2
3
4
5
6
Map<Fruit, IntSummaryStatistics> collect = listOfFruits
        .stream()
        .collect(Collectors.groupingBy(
                fruit -> fruit,
                Collectors.summarizingInt(f -> f.amount)
        ));

When you print the map you can see the count, sum, min, average, and max value for each of the groups.

1
2
3
4
{Apple=IntSummaryStatistics{count=2, sum=4, min=1, average=2.000000, max=3},
Pear=IntSummaryStatistics{count=1, sum=0, min=0, average=0.000000, max=0},
Orange=IntSummaryStatistics{count=1, sum=4, min=4, average=4.000000, max=4},
Banana=IntSummaryStatistics{count=3, sum=9, min=2, average=3.000000, max=5}}

Conclusion

We implemented different kinds of groupingBy in this post. We saw how to group by one or more fields, get the size of the groups and to get some basic statistics.