groupingBy() and partitioningBy()
The most important Stream topic — grouping by a key, downstream collectors, counting/summing/averaging per group, multi-level grouping and true/false partitions.
#Introduction
This is the topic that separates average Java developers from strong Java 8 developers. If you're interviewing for 5–8 years of Java experience, there's a very high chance you'll be asked at least one groupingBy() question.
By the end of this chapter you'll master Collectors.groupingBy(), Collectors.partitioningBy(), downstream collectors, multi-level grouping, counting / summing / averaging / max / min per group, and mapping inside grouping.
#What is groupingBy()?
Think of SQL:
SELECT department, COUNT(*)
FROM employee
GROUP BY department;In Java Streams, that's:
employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));groupingBy() groups elements based on a key.
#Basic Grouping
Map<String, List<Employee>> result = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));IT -> [John, Alex]
HR -> [David, Bob]
Sales -> [Mary]The return type is:
Map<Key, List<Value>>#Downstream Collectors
The second argument of groupingBy() decides what to do with each group.
Count Employees by Department
Map<String, Long> result = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.counting()
));IT 4
HR 2
Sales 3One of the most common interview questions.
Sum Salary by Department
Map<String, Double> result = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.summingDouble(Employee::getSalary)
));IT 320000
HR 90000
Sales 210000Average Salary by Department
Map<String, Double> avg = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(Employee::getSalary)
));IT 80000
HR 45000
Sales 70000Employee Names by Department
Suppose we only need names:
Map<String, List<String>> result = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.mapping(Employee::getName, Collectors.toList())
));IT -> [John, Alex, Rahul]
HR -> [David, Bob]Notice how mapping() transforms the grouped values.
Highest Salary by Department
Interview favourite.
Map<String, Optional<Employee>> result = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.maxBy(Comparator.comparing(Employee::getSalary))
));IT -> Optional[Alex]
HR -> Optional[David]Why Optional<Employee>? Because maxBy() is a general-purpose collector and must handle an empty input.
Lowest Salary by Department
Collectors.minBy(Comparator.comparing(Employee::getSalary))Same idea.
Summary Statistics per Department
Map<String, DoubleSummaryStatistics> stats = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.summarizingDouble(Employee::getSalary)
));Each department gets count, sum, min, max and average in one pass.
#Multi-Level Grouping
Suppose we want Department → Age → Employees:
Map<String, Map<Integer, List<Employee>>> result = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(Employee::getAge)
));- Employees
- IT
- 25John, Alex
- 30Rahul
- HR
- 28David, Bob
#partitioningBy()
Unlike groupingBy(), which can have many groups, partitioningBy() always creates exactly two groups. The keys are true and false.
Map<Boolean, List<Employee>> result = employees.stream()
.collect(Collectors.partitioningBy(e -> e.getSalary() > 50000));groupingBy() vs partitioningBy()
| groupingBy() | partitioningBy() |
|---|---|
| Any number of groups | Exactly 2 groups |
| Key can be anything | Key is boolean |
| e.g. Department, City | e.g. Salary > 50000, Age > 18 |
Rule of thumb: if your grouping condition is a yes/no question, use partitioningBy().
Partition + Count
Map<Boolean, Long> result = employees.stream()
.collect(Collectors.partitioningBy(
e -> e.getSalary() > 50000,
Collectors.counting()
));true 5
false 3#Real Spring Boot Examples
// Employees by department
employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
// Orders by status
orders.stream()
.collect(Collectors.groupingBy(Order::getStatus));
// Products by category
products.stream()
.collect(Collectors.groupingBy(Product::getCategory));
// Active vs inactive users
users.stream()
.collect(Collectors.partitioningBy(User::isActive));#Advanced Pipeline
Requirement: group by department, sort employee names alphabetically, return names only.
Map<String, List<String>> result = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.mapping(
Employee::getName,
Collectors.collectingAndThen(
Collectors.toList(),
list -> list.stream().sorted().toList()
)
)
));This is interview-level code.
#Interview Questions
Q1Difference between groupingBy() and partitioningBy()?
groupingBy()→ many groups.partitioningBy()→ exactly two groups (true/false).
Q2How do you count employees in each department?
Collectors.groupingBy(Employee::getDepartment, Collectors.counting())Q3How do you find the highest salary in each department?
Use Collectors.maxBy(...) as the downstream collector.
Q4How do you group only employee names?
Use Collectors.mapping(...) as the downstream collector.
Q5What is a downstream collector?
A collector applied inside another collector:
Collectors.groupingBy(Employee::getDepartment, Collectors.counting())Here, counting() is the downstream collector.
#Practice Problems
class Employee {
int id;
String name;
String department;
int age;
double salary;
}- Group employees by department.
- Count employees in each department.
- Find total salary by department.
- Find average salary by department.
- Find maximum salary by department.
- Find minimum salary by department.
- Group employee names by department.
- Partition employees into salary > 50,000 and others.
- Count employees in each partition.
- Group by department, then by age.
5+ Years Interview Challenge
Create a Map<String, List<String>> where key = department and value = employee names, sorted alphabetically with no duplicates.
Map<String, List<String>> result = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.collectingAndThen(
Collectors.mapping(
Employee::getName,
Collectors.toCollection(TreeSet::new)
),
ArrayList::new
)
));Why this works:
groupingBy()creates groups by department.mapping()extracts only employee names.toCollection(TreeSet::new)removes duplicates and sorts names alphabetically.collectingAndThen()converts theTreeSetinto anArrayList.
A compact, production-quality solution that combines multiple collectors elegantly.
#Pro Tips
- Use
groupingBy()instead of manually building maps with loops whenever it improves readability. - Choose the right downstream collector (
counting(),mapping(),summingDouble(), etc.) to avoid extra stream traversals. - Think in terms of SQL. If you can express a
GROUP BYquery in SQL, you can usually express it withCollectors.groupingBy().
#Chapter Summary
groupingBy()returnsMap<K, List<V>>by default- Downstream collectors:
counting(),summingDouble(),averagingDouble(),mapping(),maxBy(),minBy(),summarizingDouble() - Multi-level grouping
partitioningBy()— exactly two groups, both keys always presentgroupingBy()vspartitioningBy()