Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

The Ultimate Guide to the Java Stream API groupingBy() Collector

DZone's Guide to

The Ultimate Guide to the Java Stream API groupingBy() Collector

Still a bit puzzled by what the groupingBy() collector can do in the Java Stream API? Check out this guide on using the groupingBy() collector to its fullest potential.

· Java Zone ·
Free Resource

Get the Edge with a Professional Java IDE. 30-day free trial.

The groupingBy() is one of the most powerful and customizable Stream API collectors.

If you constantly find yourself not going beyond the following use of the  groupingBy():

.collect(groupingBy(...));


Or, if you simply wanted to discover its potential uses, then this article is for you!

Overview

Simply put, groupingBy() provides similar functionality to SQL's GROUP BY clause, only it is for the Java Stream API.

In order to use it, we always need to specify a property by which the grouping would be performed. We do this by providing an implementation of a functional interface — usually by passing a lambda expression.

For example, if we wanted to group Strings by their lengths, we could do that by passing  String::length to the  groupingBy():

List<String> strings = List.of("a", "bb", "cc", "ddd"); 

Map<Integer, List<String>> result = strings.stream() 
  .collect(groupingBy(String::length)); 

System.out.println(result); // {1=[a], 2=[bb, cc], 3=[ddd]}


But, the collector itself is capable of doing much more than simple groupings, as shown above.

Grouping Into a Custom Map Implementation

If you need to provide a custom Map implementation, you can do that by using a provided  groupingBy() overload:

List<String> strings = List.of("a", "bb", "cc", "ddd");

TreeMap<Integer, List<String>> result = strings.stream()
  .collect(groupingBy(String::length, TreeMap::new, toList()));

System.out.println(result); // {1=[a], 2=[bb, cc], 3=[ddd]}


Providing a Custom Downstream Collection

If you need to store grouped elements in a custom collection, this can be achieved by using a  toCollection()  collector.

For example, if you wanted to group elements in TreeSet instances, this could be as easy as:

groupingBy(String::length, toCollection(TreeSet::new))


And here, we have a complete example:

List<String> strings = List.of("a", "bb", "cc", "ddd");

Map<Integer, TreeSet<String>> result = strings.stream()
  .collect(groupingBy(String::length, toCollection(TreeSet::new)));

System.out.println(result); // {1=[a], 2=[bb, cc], 3=[ddd]}


Grouping and Counting Items in Groups

If you simply want to know the number of grouped elements, this can be as easy as providing a custom counting() collector:

groupingBy(String::length, counting())


Here is a complete example:

List<String> strings = List.of("a", "bb", "cc", "ddd");

Map<Integer, Long> result = strings.stream()
  .collect(groupingBy(String::length, counting()));

System.out.println(result); // {1=1, 2=2, 3=1}


Grouping and Combining Items as Strings

If you need to group elements and create a single String representation of each group, this can be achieved by using the joining() collector:

groupingBy(String::length, joining(",", "[", "]"))


And, to see this further in action, check out the following example:

List<String> strings = List.of("a", "bb", "cc", "ddd");

Map<Integer, String> result = strings.stream()
  .collect(groupingBy(String::length, joining(",", "[", "]")));

System.out.println(result); // {1=[a], 2=[bb,cc], 3=[ddd]}


Grouping and Filtering Items

Sometimes, there might be a need to exclude some items from grouped results. This can be achieved using the filtering() collector:

groupingBy(String::length, filtering(s -> !s.contains("c"), toList()))


Here is that collector in action:

List<String> strings = List.of("a", "bb", "cc", "ddd");

Map<Integer, List<String>> result = strings.stream()
  .collect(groupingBy(String::length, filtering(s -> !s.contains("c"), toList())));

System.out.println(result); // {1=[a], 2=[bb], 3=[ddd]}


Grouping and Calculating an Average per Group

If there's a need to derive an average of properties of grouped items, there are a few handy collectors for that:

  •  averagingInt() 
  •  averagingLong() 
  •  averagingDouble() 

Here are those collectors in action:

List<String> strings = List.of("a", "bb", "cc", "ddd");

Map<Integer, Double> result = strings.stream()
  .collect(groupingBy(String::length, averagingInt(String::hashCode)));

System.out.println(result); // {1=97.0, 2=3152.0, 3=99300.0}


Disclaimer:  String::hashCode was used as a placeholder.

Grouping and Calculating a Sum per Group

If you want to derive a sum from properties of grouped elements, there're some options for this as well:

  •  summingInt() 
  •  summingLong() 
  •  summingDouble() 

Here are those collectors in action:

List<String> strings = List.of("a", "bb", "cc", "ddd");

Map<Integer, Integer> result = strings.stream()
  .collect(groupingBy(String::length, summingInt(String::hashCode)));

System.out.println(result); // {1=97, 2=6304, 3=99300}


Disclaimer:  String::hashCode was used as a placeholder.

Grouping and Calculating a Statistical Summary per Group

If you want to group and then derive a statistical summary from properties of grouped items, there are out-of-the-box options for that as well:

  •  summarizingInt() 
  •  summarizingLong() 
  •  summarizingDouble() 

Here are the above collectors in action:

List<String> strings = List.of("a", "bb", "cc", "ddd");

Map<Integer, IntSummaryStatistics> result = strings.stream()
  .collect(groupingBy(String::length, summarizingInt(String::hashCode)));

System.out.println(result);


Let's take a look at the result (user-friendly reformatted):

{
    1=IntSummaryStatistics{
      count=1, 
      sum=97, 
      min=97, 
      average=97.000000, 
      max=97}, 
    2=IntSummaryStatistics{
      count=2, 
      sum=6304, 
      min=3136, 
      average=3152.000000, 
      max=3168}, 
    3=IntSummaryStatistics{
      count=1, 
      sum=99300, 
      min=99300, 
      average=99300.000000, 
      max=99300}
}


Disclaimer:  String::hashCode was used as a placeholder.

Grouping and Reducing Items

If you want to perform a reduction operation on grouped elements, you can use the reducing()collector:

groupingBy(List::size, reducing(List.of(), (l1, l2) -> ...)))


Here is an example of the reducing() collector:

List<String> strings = List.of("a", "bb", "cc", "ddd");

Map<Integer, List<Character>> result = strings.stream()
  .map(toStringList())
  .collect(groupingBy(List::size, reducing(List.of(), (l1, l2) -> Stream.concat(l1.stream(), l2.stream())
    .collect(Collectors.toList()))));

System.out.println(result); // {1=[a], 2=[b, b, c, c], 3=[d, d, d]}


Grouping and Calculating a Max/Min Item

If you want to derive a max/min element from a group, you can simply use the  max()/min()collector:

groupingBy(String::length, Collectors.maxBy(Comparator.comparing(String::toUpperCase)))


Here are those collectors in action:

List<String> strings = List.of("a", "bb", "cc", "ddd");

Map<Integer, Optional<String>> result = strings.stream()
  .collect(groupingBy(String::length, Collectors.maxBy(Comparator.comparing(String::toUpperCase))));

System.out.println(result); // {1=Optional[a], 2=Optional[cc], 3=Optional[ddd]}


The fact that the collector returns an Optional is a bit inconvenient in this case. There's always at least a single element in a group, so the usage of Optional just increases accidental complexity.

Unfortunately, there's nothing we can do with the collector to prevent it. We can recreate the same functionality using the reducing() collector, though.

Composing Downstream Collectors

The whole power of the collector gets unleashed once we start combining multiple collectors to define complex downstream grouping operations, which start resembling standard Stream API pipelines — the sky's the limit here.

Example #1

Let's say we have a list of Strings and want to obtain a map of String lengths associated with uppercased strings with a length bigger than one and collect them into a TreeSet  instance.

We can do that quite easily:

var result = strings.stream()
  .collect(
    groupingBy(String::length,
      mapping(String::toUpperCase,
        filtering(s -> s.length() > 1,
          toCollection(TreeSet::new)))));

//result
{1=[], 2=[BB, CC], 3=[DDD]}


Example #2

Given a list of Strings, group them by their matching lengths, convert them into a list of characters, flatten the obtained list, keep only distinct elements with non-zero length, and eventually reduce them by applying string concatenation.

We can achieve that as well:

var result = strings.stream()
  .collect(
    groupingBy(String::length,
      mapping(toStringList(),
        flatMapping(s -> s.stream().distinct(),
          filtering(s -> s.length() > 0,
            mapping(String::toUpperCase,
              reducing("", (s, s2) -> s + s2)))))
    ));

//result 
{1=A, 2=BC, 3=D}


Sources

All the above examples can be found in my GitHub project.

Get the Java IDE that understands code & makes developing enjoyable. Level up your code with IntelliJ IDEA. Download the free trial.

Topics:
java ,streams ,api ,collectors ,guide ,code ,grouping ,groupingby

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}