Definition
- groupingBy(classifier): groups into
Mapusing the classifier function.> - groupingBy(classifier, downstream): replaces List with a custom downstream:
counting(),summingDouble(),mapping(),maxBy(),groupingBy()(nested). - groupingBy(classifier, mapFactory, downstream): controls Map implementation (TreeMap for sorted keys).
- partitioningBy(Predicate): special boolean grouping. Guarantees both
trueandfalsekeys. - toMap: requires merge function for duplicate keys.
- collectingAndThen: wraps/transforms the collector result.
groupingBy is the cheque sorting machine at National Bank of India. Drop 1,000 cheques in. groupingBy(symbol) sorts into bins labelled RELIANCE, TCS, HDFC. groupingBy(symbol, counting()) shows the count in each bin. groupingBy(symbol, groupingBy(side)) sorts by symbol first, then by BUY/SELL within each symbol.
Understanding the Concepts
How does groupingBy work?
package com.bank.service;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
public class GroupingByDemo {
record Transaction(String symbol, String side, BigDecimal amount) {}
public static void main(String[] args) {
List<Transaction> txns = List.of(
new Transaction("RELIANCE", "BUY", new BigDecimal("45000")),
new Transaction("TCS", "BUY", new BigDecimal("78000")),
new Transaction("RELIANCE", "SELL", new BigDecimal("32000")),
new Transaction("HDFC", "BUY", new BigDecimal("120000")),
new Transaction("TCS", "SELL", new BigDecimal("55000"))
);
// Basic: group by symbol
Map<String, List<Transaction>> bySymbol = txns.stream()
.collect(Collectors.groupingBy(Transaction::symbol));
System.out.println("By symbol: " + bySymbol.keySet());
}
}
How do you use downstream collectors?
// Downstream: count per symbol
Map<String, Long> countBySymbol = txns.stream()
.collect(Collectors.groupingBy(Transaction::symbol, Collectors.counting()));
System.out.println("Count: " + countBySymbol);
// {RELIANCE=2, TCS=2, HDFC=1}
How do you do multi-level grouping?
// Multi-level: symbol then side
Map<String, Map<String, List<Transaction>>> nested = txns.stream()
.collect(Collectors.groupingBy(Transaction::symbol,
Collectors.groupingBy(Transaction::side)));
System.out.println("Nested keys: " + nested.get("RELIANCE").keySet());
// [BUY, SELL]
How do you get sorted keys with TreeMap?
// Sorted keys: TreeMap
Map<String, Long> sorted = txns.stream()
.collect(Collectors.groupingBy(Transaction::symbol,
TreeMap::new, Collectors.counting()));
System.out.println("Sorted: " + sorted);
// {HDFC=1, RELIANCE=2, TCS=2}
What are common downstream collectors?
| Downstream | Result | Example |
|-----------|--------|--------|
| counting() | Long | Count per group |
| summingDouble(fn) | Double | Sum per group |
| mapping(fn, toList()) | List
| maxBy(comparator) | Optional
| groupingBy(fn) | Map
| joining(", ") | String | Concatenation |
| collectingAndThen(d, f) | R2 | Post-process result |
What about partitioningBy?
Map<Boolean, List<Transaction>> buyVsSell = txns.stream()
.collect(Collectors.partitioningBy(t -> "BUY".equals(t.side())));
// {true: [buy txns], false: [sell txns]}
// Guarantees both keys exist, even if one group is empty
partitioningBy always has both true and false keys, unlike groupingBy which omits empty groups.
What about toMap and duplicate keys?
import java.util.stream.Collectors;
// WRONG: duplicate keys throw IllegalStateException
// Map<String, BigDecimal> bad = txns.stream()
// .collect(Collectors.toMap(Transaction::symbol, Transaction::amount));
// RELIANCE appears twice -> IllegalStateException!
// CORRECT: provide merge function
Map<String, BigDecimal> totals = txns.stream()
.collect(Collectors.toMap(
Transaction::symbol,
Transaction::amount,
BigDecimal::add)); // merge: sum duplicate key amounts
// {RELIANCE=77000, TCS=133000, HDFC=120000}
What about Collectors.joining?
String csv = txns.stream()
.map(Transaction::symbol)
.distinct()
.collect(Collectors.joining(", "));
System.out.println("CSV: " + csv);
// CSV: RELIANCE, TCS, HDFC
What should a senior engineer know?
- For the toMap duplicate key pitfall, always provide a merge function:
toMap(keyMapper, valueMapper, mergeFunction). - For sorted keys, pass TreeMap::new as the map factory.
- collectingAndThen post-processes:
collectingAndThen(toList(), Collections::unmodifiableList)produces an immutable result. - For parallel streams, groupingByConcurrent returns ConcurrentHashMap with better parallel performance but no order guarantees.
- Nested groupingBy demonstrates Collector composition, a common interview exercise.
Visual Overview
groupingBy levels:
─────────────────
Level 1: groupingBy(symbol)
→ {RELIANCE: [T1, T3], TCS: [T2, T5], HDFC: [T4]}
Level 1 + downstream: groupingBy(symbol, counting())
→ {RELIANCE: 2, TCS: 2, HDFC: 1}
Level 2: groupingBy(symbol, groupingBy(side))
→ {RELIANCE: {BUY: [T1], SELL: [T3]},
TCS: {BUY: [T2], SELL: [T5]},
HDFC: {BUY: [T4]}}
Key Takeaway
groupingBy is a terminal collector that partitions stream elements into a Map. The simplest form produces Map
For sorted keys, pass TreeMap::new as the map factory. For boolean grouping, use partitioningBy which guarantees both true and false keys. For toMap, always provide a merge function to handle duplicate keys.
Remember one sentence: groupingBy partitions into Map
---