Back to Stream: Grouping & Partitioning
Q12Question 12 of 22

Group employees by age range using a custom classifier

Key Concepts
  • groupingBy accepts any function as a classifier, not just a simple getter
  • A lambda that maps age to a band label like '20-29' creates custom range groups
  • The classifier must be a total function: it must return a key for every possible input
  • Combine with downstream collectors like counting() for counts per band
  • Use TreeMap::new for sorted band order in the output
  • An enum classifier is cleaner and safer than string labels for well-defined bands

Definition

Pass a lambda that converts age into a band label as the groupingBy classifier to bucket employees by range.
  • 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.

JAVA
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(", "))));
    }
}
TEXT
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.

JAVA
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));
TEXT
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.

JAVA
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.

JAVA
// 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.

JAVA
// 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.

JAVA
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?

DIAGRAM
- "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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate