Definition
- 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)givesMap(whole objects).> - With mapping:
groupingBy(department, mapping(getName, toList()))givesMap(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?
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);
}
}
{MUMBAI=[Rajesh, Priya], DELHI=[Amit, Kavita], CHENNAI=[Suresh]}
How do you use mapping with other downstream collectors?
// 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.
// 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?
mappingwithreducingachieves per-group aggregation on projected fields in a single collect call.flatMapping(Java 9) is the downstream equivalent offlatMap(): it flattens a nested collection within each group.- For simple extraction, consider whether
Collectors.toMapwith a value mapper might be simpler thangroupingBywithmapping.
Visual Overview
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.
---