Definition
- **Group by field**: `groupingBy(Employee::getCity)` creates `Map<String, List<Employee>>`. - **Count per group**: `groupingBy(Employee::getDepartment, counting())` gives `Map<String, Long>`. - **Filter then group**: `filter(e -> "Mumbai".equals(e.getCity()))` then `groupingBy(Employee::getDepartment)`. - **Custom classifier**: `groupingBy(e -> (e.getAge() / 10) * 10)` groups by age decade. - **Mapping downstream**: `groupingBy(dept, mapping(Employee::getName, toList()))` produces names per department.
Grouping is the bank sorting all employees into department-labelled folders. Each folder has a list of employees. Filtering picks employees from Mumbai only or employees above age 50. Combined: give me Mumbai employees grouped by department is filter first, then group.
Understanding the Concepts
How do you group and filter employees with streams?
package com.bank.service;
import java.util.*;
import java.util.stream.Collectors;
public class EmployeeGrouping {
record Employee(String name, String department, String city, int age, double salary) {}
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Rajesh", "IT", "Mumbai", 35, 85000),
new Employee("Priya", "IT", "Delhi", 28, 72000),
new Employee("Amit", "Finance", "Mumbai", 42, 95000),
new Employee("Deepa", "Finance", "Chennai", 55, 110000),
new Employee("Vikram", "IT", "Mumbai", 31, 78000)
);
// Group by city
Map<String, List<Employee>> byCity = employees.stream()
.collect(Collectors.groupingBy(Employee::city));
System.out.println("Cities: " + byCity.keySet());
// Count by department
Map<String, Long> countByDept = employees.stream()
.collect(Collectors.groupingBy(Employee::department, Collectors.counting()));
System.out.println("Dept count: " + countByDept);
}
}
Cities: [Mumbai, Delhi, Chennai]
Dept count: {IT=3, Finance=2}
How do you filter then group?
// Filter Mumbai, then group by department
Map<String, List<Employee>> mumbaiByDept = employees.stream()
.filter(e -> "Mumbai".equals(e.city()))
.collect(Collectors.groupingBy(Employee::department));
System.out.println("Mumbai depts: " + mumbaiByDept.keySet());
// {IT=[Rajesh, Vikram], Finance=[Amit]}
Filter first, then group. This avoids grouping all employees and then filtering each group.
How do you filter by age?
// Filter age > 40
List<Employee> seniors = employees.stream()
.filter(e -> e.age() > 40)
.toList();
System.out.println("Seniors: " + seniors.stream().map(Employee::name).toList());
// [Amit, Deepa]
How do you group by age decade?
// Group by age decade
Map<Integer, List<Employee>> byDecade = employees.stream()
.collect(Collectors.groupingBy(e -> (e.age() / 10) * 10));
System.out.println("Decades: " + byDecade.keySet());
// [30, 20, 40, 50]
How do you extract names per department?
// Names per department (mapping downstream)
Map<String, List<String>> namesByDept = employees.stream()
.collect(Collectors.groupingBy(Employee::department,
Collectors.mapping(Employee::name, Collectors.toList())));
System.out.println("IT names: " + namesByDept.get("IT"));
// [Rajesh, Priya, Vikram]
How do you get sorted keys with TreeMap?
// Sorted keys: TreeMap factory
Map<String, Long> sorted = employees.stream()
.collect(Collectors.groupingBy(Employee::city,
TreeMap::new, Collectors.counting()));
System.out.println("Sorted: " + sorted);
// {Chennai=1, Delhi=1, Mumbai=3}
What are common mistakes?
// WRONG: grouping all employees first, then filtering each group
Map<String, List<Employee>> all = employees.stream()
.collect(Collectors.groupingBy(Employee::city));
List<Employee> mumbai = all.get("Mumbai"); // two-step, inefficient
// CORRECT: filter first, then group
Map<String, List<Employee>> mumbaiByDept = employees.stream()
.filter(e -> "Mumbai".equals(e.city()))
.collect(Collectors.groupingBy(Employee::department));
What should a senior engineer know?
- Each grouping replaces a manual HashMap loop. The filter-then-group pattern avoids grouping all employees and then filtering each group.
- The mapping downstream produces lightweight results (names instead of full objects).
- TreeMap::new as the map factory produces alphabetically sorted keys.
- For multi-level grouping, nest groupingBy inside groupingBy.
- In production, push filter+group to database GROUP BY for large datasets.
Visual Overview
Employee Stream Patterns: ───────────────────────── Group by city: groupingBy(Employee::city) → Map<City, List<Emp>> Count by dept: groupingBy(dept, counting()) → Map<Dept, Long> Filter + group: filter(Mumbai).groupingBy(dept) → Map<Dept, List<Emp>> Names per dept: groupingBy(dept, mapping(name)) → Map<Dept, List<String>> Age decade: groupingBy(age/10*10) → Map<Int, List<Emp>>
Key Takeaway
Grouping uses groupingBy with a classifier function. Filtering uses filter with a predicate. The powerful combination is filter then group: filter Mumbai employees first, then group by department. Each replaces a manual HashMap loop with a single declarative collect call.
Downstream collectors customise the grouped values: counting for headcount, mapping for extracting specific fields, and nested groupingBy for multi-level grouping.
Remember one sentence: filter first to narrow the dataset, then groupingBy to partition, with downstream collectors to customise each group.
---