Back to Collectors
Q1Question 1 of 21

Collectors.groupingBy(): single-level, multi-level, downstream collectors

Key Concepts
  • groupingBy(classifier) groups elements into Map<K, List<T>> based on a classification function.
  • groupingBy(classifier, downstream) replaces the default List with a custom downstream collector (counting, summing, mapping).
  • groupingBy(classifier, mapFactory, downstream) controls the Map type (TreeMap for sorted keys).
  • Multi-level grouping nests groupingBy inside groupingBy: Map<K1, Map<K2, List<T>>>.
  • partitioningBy is the special case for boolean grouping: guarantees both true and false keys exist.
  • Collectors.toMap with duplicate keys throws IllegalStateException unless a merge function is provided.
  • collectingAndThen post-processes the collector result: useful for wrapping in unmodifiable collections.

Definition

groupingBy(classifier) produces Map<K, List<T>>; add a downstream for counting, summing, or nested groupingBy for multi-level; use TreeMap::new for sorted keys.
  • groupingBy(classifier): groups into Map> using 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 true and false keys.
  • 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?

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

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

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

JAVA
// 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 | Extract field per group |
| maxBy(comparator) | Optional | Largest per group |
| groupingBy(fn) | Map> | Multi-level grouping |
| joining(", ") | String | Concatenation |
| collectingAndThen(d, f) | R2 | Post-process result |

What about partitioningBy?

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

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

JAVA
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

DIAGRAM
  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>. Adding a downstream collector replaces the List with counting, summing, mapping, or nested groupingBy for multi-level grouping.

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>, downstream collectors customise the values, and nesting groupingBy creates multi-level maps.

---

Share
Use ← → arrow keys to navigate