Definition
- groupingBy(dept) buckets employees by department.
- mapping(city, toSet()) downstream extracts each employee's city and collects into a
Set. - toSet() deduplicates: each city appears once per department.
- Without mapping, the values would be
Listinstead ofSet. - For sorted cities, use
toCollection(TreeSet::new)instead oftoSet().
At the National Bank of India, Priya creates a coverage report showing which cities each department has staff in. For each department, she lists the distinct cities, with each city appearing only once regardless of how many employees are based there.
Understanding the Concepts
What does this collect achieve?
This produces a Map where each key is a department name and each value is the set of unique cities where that department has employees. The mapping downstream collector is the key: it transforms each employee into their city string before collecting into a set.
How do you implement it?
package com.bank.hr;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
public class DeptToCities {
record Employee(String id, String name, String department,
String city, BigDecimal salary) {}
public static void main(String[] args) {
List<Employee> staff = List.of(
new Employee("SAV2001", "Rajesh", "TRADING", "Mumbai", new BigDecimal("1800000")),
new Employee("SAV2002", "Priya", "TRADING", "Delhi", new BigDecimal("1200000")),
new Employee("SAV2003", "Meera", "TRADING", "Mumbai", new BigDecimal("1500000")),
new Employee("SAV2004", "Amit", "RISK", "Mumbai", new BigDecimal("900000")),
new Employee("SAV2005", "Kavita", "RISK", "Chennai", new BigDecimal("1100000")),
new Employee("SAV2006", "Ananya", "COMPLIANCE", "Delhi", new BigDecimal("1050000"))
);
Map<String, Set<String>> deptCities = staff.stream()
.filter(e -> e.department() != null && e.city() != null)
.collect(Collectors.groupingBy(
Employee::department,
Collectors.mapping(Employee::city, Collectors.toSet())));
deptCities.forEach((dept, cities) ->
System.out.println(dept + ": " + cities));
}
}
TRADING: [Delhi, Mumbai]
RISK: [Chennai, Mumbai]
COMPLIANCE: [Delhi]
Note that TRADING has two employees in Mumbai but Mumbai appears only once because toSet() deduplicates.
What does the mapping collector do?
- The
mapping(mapper, downstream)collector transforms each element with the mapper function before passing it to the further downstream collector. Here: mapperisEmployee::city(extracts the city string).downstreamistoSet()(collects into a set).
Without mapping, you would collect employees, not cities.
// Without mapping: Map<String, List<Employee>>
Map<String, List<Employee>> withoutMapping = staff.stream()
.collect(Collectors.groupingBy(Employee::department));
// With mapping: Map<String, Set<String>>
Map<String, Set<String>> withMapping = staff.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.mapping(Employee::city, Collectors.toSet())));
How do you get sorted unique cities?
Map<String, TreeSet<String>> sortedCities = staff.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.mapping(Employee::city,
Collectors.toCollection(TreeSet::new))));
// Each department's cities in alphabetical order
How is mapping different from just map on the stream?
| Approach | Where transformation happens | Grouping preserved? |
|---|---|---|
| stream().map(city).collect(groupingBy(...)) | Before grouping | No: you lose the employee |
| groupingBy(dept, mapping(city, toSet())) | Inside each group | Yes: department grouping intact |
The mapping downstream applies the transformation per group, keeping the grouping structure.
What are common mistakes?
Mistake: Forgetting the mapping step and getting the wrong value type.
// WRONG: values are List<Employee>, not Set<String>
Map<String, List<Employee>> wrong = staff.stream()
.collect(Collectors.groupingBy(Employee::department));
// CORRECT: mapping extracts city, toSet deduplicates
Map<String, Set<String>> correct = staff.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.mapping(Employee::city, Collectors.toSet())));
What about counting distinct cities per department?
Chain collectingAndThen to count after collecting.
Map<String, Long> cityCountPerDept = staff.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.collectingAndThen(
Collectors.mapping(Employee::city, Collectors.toSet()),
set -> (long) set.size())));
What should a senior engineer know?
Composing a transforming downstream collector with mapping is the general way to collect a derived attribute per group. It adapts the element type before the terminal collector, enabling groupingBy to produce per-group values of any shape: sets, lists, counts, or concatenated strings.
What are common interview follow-up questions?
- "How do you get sorted cities?" Replace
toSet()withtoCollection(TreeSet::new). - "How do you count distinct cities per department?" Wrap with
collectingAndThenand callsize(). - "What if you want names instead of cities?" Change the mapping function to
Employee::name. - "Can you compose multiple downstream collectors?" Yes, nesting
mapping,filtering, and other collectors.
Visual Overview
Input: [Rajesh/TRADING/Mumbai, Priya/TRADING/Delhi,
Meera/TRADING/Mumbai, Amit/RISK/Mumbai,
Kavita/RISK/Chennai, Ananya/COMPLIANCE/Delhi]
│
│ groupingBy(department)
│ mapping(city, toSet())
▼
┌──────────────────────────────────────┐
│ TRADING → {Mumbai, Delhi} │
│ (Mumbai deduplicated) │
│ RISK → {Mumbai, Chennai} │
│ COMPLIANCE → {Delhi} │
└──────────────────────────────────────┘
Key Takeaway
To build a map of department to its set of distinct cities, group by department and use a mapping downstream collector that extracts the city and collects into a set. The mapping collector transforms each employee into their city string before toSet() deduplicates, so each department's value is its distinct cities.
At the National Bank of India, Priya uses this for a coverage report showing where each department has staff. Mumbai appears only once for TRADING even though two employees are based there. For sorted cities, she replaces toSet() with toCollection(TreeSet::new).
The mapping downstream is the general way to collect a derived attribute per group. Without it, groupingBy collects the original elements (employees), not the derived values (cities). This group-then-map-to-set pattern is reusable for any per-group distinct attribute set.
Remember one sentence: groupingBy(dept, mapping(city, toSet())) collects distinct city names per department by transforming elements inside each group.
---