Definition
- groupingBy accepts any function from element to key, not just a stored field getter.
- A custom classifier derives the key from each element: age to band label, salary to tier, etc.
- The classifier must be a total function: it must return a key for every possible input and never throw.
- Use TreeMap::new for sorted band keys.
- An enum classifier is safer than string labels for a fixed set of categories.
Priya at the National Bank of India does not want one tray per exact age; she wants broad bands: twenties, thirties, forties, fifty-plus. She writes a rule that reads the age and decides which band tray each file goes into.
Understanding the Concepts
How do you group by a custom range?
groupingBy accepts any function from element to key. Instead of a getter, pass a classifier that computes a derived key.
package com.bank.hr;
import java.util.*;
import java.util.stream.Collectors;
public class GroupByAgeRange {
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Rajesh", 45),
new Employee("Priya", 28),
new Employee("Amit", 35),
new Employee("Kavita", 52),
new Employee("Neha", 29),
new Employee("Suresh", 41)
);
Map<String, List<Employee>> byAge = employees.stream()
.collect(Collectors.groupingBy(e -> {
int age = e.getAge();
if (age < 30) return "20-29";
else if (age < 40) return "30-39";
else if (age < 50) return "40-49";
else return "50+";
}));
byAge.forEach((band, emps) ->
System.out.println(band + ": " + emps.stream()
.map(Employee::getName)
.collect(Collectors.joining(", "))));
}
}
20-29: Priya, Neha
30-39: Amit
40-49: Rajesh, Suresh
50+: Kavita
How do you count employees per band?
Add counting() as a downstream collector to get counts instead of full lists.
Map<String, Long> countPerBand = employees.stream()
.collect(Collectors.groupingBy(
e -> {
int age = e.getAge();
if (age < 30) return "20-29";
else if (age < 40) return "30-39";
else if (age < 50) return "40-49";
else return "50+";
},
TreeMap::new,
Collectors.counting()));
countPerBand.forEach((band, count) ->
System.out.println(band + ": " + count));
20-29: 2
30-39: 1
40-49: 2
50+: 1
Why is an enum classifier better than strings?
String labels can have typos and no compile-time safety. An enum defines the exact set of valid bands.
enum AgeBand {
TWENTIES("20-29"), THIRTIES("30-39"),
FORTIES("40-49"), FIFTY_PLUS("50+");
final String label;
AgeBand(String label) { this.label = label; }
static AgeBand of(int age) {
if (age < 30) return TWENTIES;
if (age < 40) return THIRTIES;
if (age < 50) return FORTIES;
return FIFTY_PLUS;
}
}
Map<AgeBand, List<Employee>> byBand = employees.stream()
.collect(Collectors.groupingBy(
e -> AgeBand.of(e.getAge())));
What if the classifier does not cover all cases?
A classifier that does not return a key for every input causes elements to be lost or the stream to throw. The classifier must be total.
// WRONG: misses ages below 20
e -> {
int age = e.getAge();
if (age >= 20 && age < 30) return "20-29";
if (age >= 30 && age < 40) return "30-39";
// age 18 returns null → NullPointerException!
}
// CORRECT: always return a value
e -> {
int age = e.getAge();
if (age < 20) return "Under 20";
if (age < 30) return "20-29";
if (age < 40) return "30-39";
if (age < 50) return "40-49";
return "50+";
}
What are common mistakes?
Mistake: returning null from the classifier.
// WRONG: groupingBy throws NullPointerException if classifier returns null
employees.stream()
.collect(Collectors.groupingBy(e -> {
if (e.getAge() > 30) return "Senior";
// falls through to null for age <= 30!
}));
// CORRECT: always return a non-null key
employees.stream()
.collect(Collectors.groupingBy(e ->
e.getAge() > 30 ? "Senior" : "Junior"));
How do you group by salary tier instead of age?
The same pattern works for any derived key.
Map<String, List<Employee>> byTier = employees.stream()
.collect(Collectors.groupingBy(e -> {
double salary = e.getSalaryAsDouble();
if (salary < 50000) return "Entry";
if (salary < 80000) return "Mid";
return "Senior";
}));
What should a senior engineer know?
- The classifier must be a pure, total function: it must return a non-null key for every input and have no side effects.
- Deriving the grouping key on the fly keeps the data model clean; no need for a stored band column.
- For a fixed, well-defined set of categories, prefer an enum classifier over string labels.
- Use TreeMap::new to get bands in sorted order for display.
What are common interview follow-up questions?
- "How do you group by decade of joining year?" Use `e -> (e.getJoinYear() / 10) * 10` as the classifier. - "How do you combine age band with department?" Use nested groupingBy or a composite key. - "What if a band has zero employees?" groupingBy only creates keys for bands that have at least one element. To include empty bands, merge with a pre-built map of all bands.
Visual Overview
Stream<Employee>
[Rajesh:45, Priya:28, Amit:35, Kavita:52, Neha:29, Suresh:41]
│
│ classifier: age → band
▼
Mapping:
Rajesh (45) → "40-49"
Priya (28) → "20-29"
Amit (35) → "30-39"
Kavita (52) → "50+"
Neha (29) → "20-29"
Suresh (41) → "40-49"
│
│ groupingBy(band)
▼
Map<String, List<Employee>>
┌─────────────────────────────┐
│ 20-29 → [Priya, Neha] │
│ 30-39 → [Amit] │
│ 40-49 → [Rajesh, Suresh] │
│ 50+ → [Kavita] │
└─────────────────────────────┘
Key Takeaway
The real power of groupingBy is that its classifier can be any function, not just a simple getter. To group employees into age bands, pass a lambda that reads each age and returns a band label like '20-29' or '50+'. Each employee lands in the bucket matching the label the classifier returns.
At the National Bank of India, HR used this for workforce planning: age bands for retirement projections and training allocation. Supply a TreeMap for sorted band order and combine with counting() for head counts per band.
The classifier must be total and pure: it must return a non-null key for every input and never throw. For a fixed set of categories, prefer an enum classifier over string labels for compile-time safety.
Remember one sentence: groupingBy accepts any function as a classifier, and a custom lambda that derives range labels turns raw values into meaningful business segments.
---