Back to Collectors
Q3Question 3 of 21

Collectors.mapping: downstream transformation within groups

Key Concepts
  • Collectors.mapping(mapper, downstream) transforms elements WITHIN each group before collecting
  • groupingBy(dept, mapping(getName, toList())) produces Map<String, List<String>> instead of Map<String, List<Employee>>
  • mapping composes with any inner collector: toList, toSet, joining, reducing, counting
  • Without mapping, groupingBy always collects the original objects into each group
  • mapping is the downstream equivalent of the stream map() operation
  • Use mapping when you need to extract or transform a field within each group in a single collect call

Definition

Collectors.mapping applies a transformation to each element within a group before the downstream collector processes it, letting you group by one field but collect a different projected value.
  • mapping(mapper, downstream) transforms elements within each group before collecting.
  • groupingBy(classifier, mapping(extractor, toList())) groups by one attribute but collects a different projected value per group.
  • Without mapping: groupingBy(department) gives Map> (whole objects).
  • With mapping: groupingBy(department, mapping(getName, toList())) gives Map> (just names).
  • mapping composes with any inner collector: toList(), toSet(), joining(", "), reducing(...), counting().
  • It is the downstream equivalent of the map() intermediate operation.

At National Bank of India, groupingBy department gives you departments mapped to lists of full employee records. But the branch summary only needs names, not complete records. mapping extracts just the name from each employee before collecting, like a clerk who copies only the name column from each full form into the summary report.

Understanding the Concepts

How does mapping work as a downstream collector?

JAVA
package com.bank.analytics;

import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;

public class MappingCollectorDemo {
    record Account(String id, String holder, String branch, BigDecimal balance) {}

    public static void main(String[] args) {
        List<Account> accounts = List.of(
            new Account("SAV2001", "Rajesh", "MUMBAI",  new BigDecimal("50000")),
            new Account("SAV2002", "Priya",  "MUMBAI",  new BigDecimal("150000")),
            new Account("SAV2003", "Amit",   "DELHI",   new BigDecimal("75000")),
            new Account("SAV2004", "Kavita", "DELHI",   new BigDecimal("120000")),
            new Account("SAV2005", "Suresh", "CHENNAI", new BigDecimal("200000"))
        );

        // Without mapping: Map<String, List<Account>>
        Map<String, List<Account>> byBranch = accounts.stream()
            .collect(Collectors.groupingBy(Account::branch));

        // With mapping: Map<String, List<String>> (just holder names)
        Map<String, List<String>> holdersByBranch = accounts.stream()
            .collect(Collectors.groupingBy(
                Account::branch,
                Collectors.mapping(Account::holder, Collectors.toList())));

        System.out.println(holdersByBranch);
    }
}
TEXT
{MUMBAI=[Rajesh, Priya], DELHI=[Amit, Kavita], CHENNAI=[Suresh]}

How do you use mapping with other downstream collectors?

JAVA
// mapping + joining: comma-separated holder names per branch
Map<String, String> holdersCsv = accounts.stream()
    .collect(Collectors.groupingBy(
        Account::branch,
        Collectors.mapping(Account::holder, Collectors.joining(", "))));
// {MUMBAI=Rajesh, Priya, DELHI=Amit, Kavita, CHENNAI=Suresh}

// mapping + toSet: unique account IDs per branch
Map<String, Set<String>> idsByBranch = accounts.stream()
    .collect(Collectors.groupingBy(
        Account::branch,
        Collectors.mapping(Account::id, Collectors.toSet())));

// mapping + reducing: sum of balances per branch
Map<String, BigDecimal> totalByBranch = accounts.stream()
    .collect(Collectors.groupingBy(
        Account::branch,
        Collectors.mapping(Account::balance,
            Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));

How is mapping different from map() on the stream?

| Feature | stream.map() | Collectors.mapping() |
|---------|---------------|----------------------|
| Type | Intermediate operation | Downstream collector |
| Scope | Entire stream | Within each group |
| Use with | Any stream operation | groupingBy, partitioningBy |

stream.map() transforms before grouping. Collectors.mapping() transforms after grouping, within each group.

What are common mistakes?

WRONG: Using stream.map() before groupingBy when you want to transform within groups.

JAVA
// WRONG: map before groupBy loses the original department info
accounts.stream()
    .map(Account::holder)  // now Stream<String>, no department!
    .collect(Collectors.groupingBy(???)); // cannot group by department

CORRECT: Use mapping as a downstream.

What should a senior engineer know?

  • mapping with reducing achieves per-group aggregation on projected fields in a single collect call.
  • flatMapping (Java 9) is the downstream equivalent of flatMap(): it flattens a nested collection within each group.
  • For simple extraction, consider whether Collectors.toMap with a value mapper might be simpler than groupingBy with mapping.

Visual Overview

DIAGRAM
  accounts stream
       │
       ▼
  groupingBy(branch)
  ┌────────────────────────────────────────────┐
  │ MUMBAI:  [Account(Rajesh), Account(Priya)] │
  │ DELHI:   [Account(Amit), Account(Kavita)]  │
  │ CHENNAI: [Account(Suresh)]                 │
  └──────────────────┬─────────────────────────┘
                     │
                     ▼
  mapping(Account::holder, toList())
  ┌────────────────────────────────────────────┐
  │ MUMBAI:  ["Rajesh", "Priya"]               │
  │ DELHI:   ["Amit", "Kavita"]                │
  │ CHENNAI: ["Suresh"]                        │
  └────────────────────────────────────────────┘

Key Takeaway

Collectors.mapping is a downstream collector that transforms elements within each group before the inner downstream processes them. groupingBy(branch, mapping(holder, toList())) groups by branch and collects holder NAMES instead of full Account objects.

It composes with any inner collector: toList for lists, toSet for unique values, joining for CSV strings, reducing for aggregation. Without mapping, groupingBy always collects the original objects.

At National Bank of India, the branch summary maps each branch to a comma-separated list of account holder names using mapping(holder, joining(", ")) in a single collect call.

Remember one sentence: mapping inside groupingBy extracts and transforms a field within each group, like map() but scoped to each group.

---

Share
Use ← → arrow keys to navigate