Back to Collectors
Q19Question 19 of 21

Collect employees into a Map<String, Set<String>> (department to set of unique cities)

Key Concepts
  • groupingBy(dept, mapping(city, toSet())) gives each department its set of distinct cities
  • The mapping downstream collector transforms each element before passing it to the further collector
  • toSet() deduplicates cities automatically, so each city appears only once per department
  • Without mapping, you would collect employees, not cities, in each group
  • For sorted unique cities, replace toSet() with toCollection(TreeSet::new)
  • Guard null department and city before grouping to avoid stray null keys or entries

Definition

`groupingBy(dept, mapping(city, toSet()))` gives each department its set of unique cities in a single pass.
  • 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 List instead of Set.
  • For sorted cities, use toCollection(TreeSet::new) instead of toSet().

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?

JAVA
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));
    }
}
TEXT
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:
  • mapper is Employee::city (extracts the city string).
  • downstream is toSet() (collects into a set).

Without mapping, you would collect employees, not cities.

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

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

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

JAVA
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() with toCollection(TreeSet::new).
  • "How do you count distinct cities per department?" Wrap with collectingAndThen and call size().
  • "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

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

---

Share
Use ← → arrow keys to navigate