Java 8 Stream – Collectors GroupingBy with Examples

Learn to use Collectors.groupingBy() method to group and aggregate the Stream elements similar to ‘GROUP BY‘ clause in the SQL.

Stream -> groupingBy() -> Map of elements after applying ‘group by’ operation

1. Collectors.groupingBy() Method

1.1. Syntax

The groupingBy() method returns a Collector implementing a “GROUP BY” operation on Stream elements and returns the result as a Map.

groupingBy(classifier, collector)
groupingBy(classifier, supplier, collector)

We can pass the following arguments to this method:

  • classifier: maps input elements to map keys
  • collector: is the downstream reduction function. By default, Collectors.toList() is used which causes the grouped elements into a List.
  • supplier: provides a new empty Map into which the results will be inserted. By default, HashMap::new is used. We can use other maps such as TreeMap, LinkedHashMap or ConcurrentMap to insert additional behavior in the grouping process such as sorting.

1.2. Using groupingByConcurrent() for Parallel Processing

We can use Collectors.groupingByConcurrent() if we wish to process the stream elements parallelly that uses the multi-core architecture of the machine and returns a ConcurrentMap. Except for concurrency, it works similarly to groupingBy() method.

groupingByConcurrent(classifier, collector)
groupingByConcurrent(classifier, supplier, collector)

2. Collectors groupingBy Examples

For the demo purposes, we are creating two records Person and Department as follows.

record Person(int id, String name, double salary, Department department) {}
record Department(int id, String name) {}

And we are creating a List of persons that we will use to create Stream and collect the records in different groupings.

List<Person> persons = List.of(
    new Person(1, "Alex", 100d, new Department(1, "HR")),
    new Person(2, "Brian", 200d, new Department(1, "HR")),
    new Person(3, "Charles", 900d, new Department(2, "Finance")),
    new Person(4, "David", 200d, new Department(2, "Finance")),
    new Person(5, "Edward", 200d, new Department(2, "Finance")),
    new Person(6, "Frank", 800d, new Department(3, "ADMIN")),
    new Person(7, "George", 900d, new Department(3, "ADMIN")));

2.1. Grouping By a Simple Condition

Let us start with a simple condition to understand the usage better. In the following example, we are grouping all the persons by department. This will create a Map<Department, List<Person>> where map key is department records and map value will be all the persons in that department.

Map<Department, List<Person>> map =;


The program output.

	Department[id=2, name=Finance]=[
	Person[id=3, ...], 
	Person[id=4, ...], 
	Person[id=5, ...], 

	Department[id=3, name=ADMIN]=[
	Person[id=6, ...], 
	Person[id=7, ...], 

	Department[id=1, name=HR]=[
	Person[id=1, ...], 
	Person[id=2, ...]

Similarly, if we wish to collect only the person ids in all departments then we can use Collectors.mapping() to collect all the person ids in a List rather than collecting the person records.

Map<Department, List<Integer>> map =
        .collect(groupingBy(Person::department, mapping(Person::id, toList())));


The program output.

	Department[id=2, name=Finance]=[3, 4, 5], 
	Department[id=3, name=ADMIN]=[6, 7], 
	Department[id=1, name=HR]=[1, 2]

2.2. Grouping by Complex Condition

There may be cases when we have to apply a complex condition for grouping. In this case, the Map can represent the condition using a Java tuple and then group the matching elements as a List in Map value.

In the following example, we want to group on distinct departments and salary pairs. In the Map value, we will get a list of persons who have the same department and the same salary.

Map<Object, List<Integer>> map =
    .collect(groupingBy(person -> new Pair<>(person.salary(), person.department()),
        mapping(Person::id, toList())));


The program output clearly tells that persons 4 and 5 are in the same department and have the same salary.

	[900.0, Department[id=3, name=ADMIN]]=[7], 
	[800.0, Department[id=3, name=ADMIN]]=[6], 
	[200.0, Department[id=2, name=Finance]]=[4, 5], 
	[900.0, Department[id=2, name=Finance]]=[3], 
	[200.0, Department[id=1, name=HR]]=[2], 
	[100.0, Department[id=1, name=HR]]=[1]

2.3. Grouping with Counting

We can also aggregate the values by performing other operations such as counting(), averaging() summing() etc. This helps in getting the reduction operation on Map values to produce a single value.

In the following example, we are counting all the persons in a department.

Map<Department, Long> map =
    .collect(groupingBy(Person::department, counting()));


The program output.

	Department[id=2, name=Finance]=3, 
	Department[id=3, name=ADMIN]=2, 
	Department[id=1, name=HR]=2

In the same way, we can find count all the persons having the same salary.

Map<Double, Long> map =
    .collect(groupingBy(Person::salary, counting()));


The program output.

{800.0=1, 200.0=3, 100.0=1, 900.0=2}

2.4. Grouping with Average

It is possible to perform other aggregate operations such as finding the average salary in each department.

Map<Department, Double> map =
    .collect(groupingBy(Person::department, averagingDouble(Person::salary)));


The program output.

	Department[id=2, name=Finance]=433.3333333333333, 
	Department[id=3, name=ADMIN]=850.0, 
	Department[id=1, name=HR]=150.0

2.5. Grouping with Max/Min

To find the maximum or minimum value for the grouped values, use maxBy() or minBy() collectors and pass the Comparator argument to compare the required field values.

In the following example, we are finding the max salaried person in each department.

Map<Department, Optional<Person>> map =
    .collect(groupingBy(Person::department, maxBy(Comparator.comparingDouble(Person::salary))));


The program output.

	Department[id=2, name=Finance]=Optional[Person[id=3, name=Charles, salary=900.0,...]],
	Department[id=3, name=ADMIN]=Optional[Person[id=7, name=George, salary=900.0, ...]], 
	Department[id=1, name=HR]=Optional[Person[id=2, name=Brian, salary=200.0, ...]]

2.6. Grouping with Filtering

The Stream.filter() method filters out all the non-matching elements from the stream before passing it to the next operation. This may not be the desired solution.

Consider the following example where we are grouping all persons by the department; whose salary is greater than 300.

Map<Department, Long> map =
    .filter(p -> p.salary() > 300d)
    .collect(groupingBy(Person::department, counting()));


The program output.

	Department[id=2, name=Finance]=1, 
	Department[id=3, name=ADMIN]=2

The above program output omits the department-1 altogether because there was no person matching the condition in that department. But if we want to list all such Map keys where there is no matching value exists then we can use Collectors.filtering() method that applies the filter while adding values in to Map.

Map<Department, Long> map =
    .collect(groupingBy(Person::department, filtering(p -> p.salary() > 300d, counting())));


The program output. Now the department-1 is also listed with zero matching records.

	Department[id=2, name=Finance]=1, 
	Department[id=3, name=ADMIN]=2, 
	Department[id=1, name=HR]=0

3. Conclusion

The Collectors’s groupBy() method is excellent for grouping the Stream elements by various conditions and performing all kinds of aggregate operations on the Map values. We can use combinations of Collectors to perform any kind of grouping as shown in the above examples.

Happy Learning !!

Source Code on Github


Notify of
Most Voted
Newest Oldest
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.