Skip to content

Intermediate · Lambdas, streams and files · topic 39 of 60 · 25 min

Collectors: grouping and summarising

Explanation

collect(...) with the Collectors class turns a stream into maps and summaries: groupingBy (group by a key), counting, summingInt/summingDouble, averagingDouble, partitioningBy (split by true/false), joining (one string) and toMap. These replace many lines of map-building loops in reports.

From stream to report

Reports usually group data: sales per city, orders per status, students per grade. Collectors.groupingBy takes a function that gives each element's key and builds a Map from key to a List of elements, or to a summary if you add a second, "downstream" collector.

java
import static java.util.stream.Collectors.*;

record Sale(String city, String product, double amount) {}
List<Sale> sales = List.of(
    new Sale("Pune", "Tea", 120), new Sale("Delhi", "Tea", 80),
    new Sale("Pune", "Coffee", 200), new Sale("Goa", "Tea", 60));

Map<String, List<Sale>> byCity = sales.stream().collect(groupingBy(Sale::city));
Map<String, Long> countByCity = sales.stream().collect(groupingBy(Sale::city, counting()));
Map<String, Double> revenueByCity = sales.stream().collect(groupingBy(Sale::city, summingDouble(Sale::amount)));
// revenueByCity: {Pune=320.0, Delhi=80.0, Goa=60.0} (HashMap: order not guaranteed)

Map<String, Double> sortedRevenue = sales.stream()
        .collect(groupingBy(Sale::city, TreeMap::new, summingDouble(Sale::amount)));   // sorted by city

More collectors

java
Map<Boolean, List<Sale>> bigOrSmall = sales.stream()
        .collect(partitioningBy(s -> s.amount() >= 100));     // {false=[…], true=[…]}

String cities = sales.stream().map(Sale::city).distinct().sorted()
        .collect(joining(", ", "Cities: ", "."));           // "Cities: Delhi, Goa, Pune."

Map<String, Double> avgByProduct = sales.stream()
        .collect(groupingBy(Sale::product, averagingDouble(Sale::amount)));   // {Tea=86.66…, Coffee=200.0}

Map<String, Set<String>> productsByCity = sales.stream()
        .collect(groupingBy(Sale::city, mapping(Sale::product, toSet())));

DoubleSummaryStatistics stats = sales.stream().collect(summarizingDouble(Sale::amount));
System.out.println(stats.getMax() + " " + stats.getAverage());   // 200.0 115.0

toMap and the duplicate-key trap

java
record Product(String sku, String name, double price) {}
Map<String, Double> priceBySku = products.stream()
        .collect(toMap(Product::sku, Product::price));

// If two products share a SKU: IllegalStateException: Duplicate key …
Map<String, Double> lowestPrice = products.stream()
        .collect(toMap(Product::sku, Product::price, Math::min));   // merge function keeps the lower price

Collectors versus loops

Everything a collector does can be written with a loop and a HashMap, as you did in module 7 with merge and computeIfAbsent. Collectors are shorter and harder to get wrong for standard summaries, and they combine: groupingBy can take another groupingBy to build a two-level report (sales by city, then by product). For unusual logic with many conditions, a plain loop may still be the clearest choice; pick whatever your team can read fastest.

java
Map<String, Map<String, Double>> cityProduct = sales.stream()
        .collect(groupingBy(Sale::city, TreeMap::new,
                 groupingBy(Sale::product, TreeMap::new, summingDouble(Sale::amount))));
// {Delhi={Tea=80.0}, Goa={Tea=60.0}, Pune={Coffee=200.0, Tea=120.0}}
Careful: Collectors.toMap throws IllegalStateException when two elements produce the same key, which often shows up only with real production data. Either guarantee unique keys or pass a merge function as the third argument. Also note that groupingBy and toMap return HashMaps by default, whose iteration order is unspecified; use a TreeMap or LinkedHashMap supplier when order matters.
Think of it like this: groupingBy is like sorting the day's post into pigeonholes by PIN code; a downstream collector is what you do inside each pigeonhole: count the letters (counting), weigh them (summing), or just keep them in a bundle (toList).
Tip: Read nested collectors from the outside in: groupingBy(city, summingDouble(amount)) = "for each city, sum the amounts". Format long collector chains over several lines.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Collectors: grouping and summarising · 1/6

Collectors

In this lesson you will turn streams into reports with Collectors.

Prefer a video by a teacher? Find free videos on “java collectors groupingby counting partitioningby tomap joining” on YouTube ↗

In real life: A monthly GST summary

An accountant's tool groups a month's invoices by GST rate and sums the taxable value and tax for each rate, the way returns are summarised. One stream with groupingBy and a TreeMap gives the table in rate order.

java
record Invoice(int gstRate, double taxable) { double tax() { return taxable * gstRate / 100; } }
Map<Integer, Double> taxByRate = invoices.stream()
        .collect(Collectors.groupingBy(Invoice::gstRate, TreeMap::new, Collectors.summingDouble(Invoice::tax)));
taxByRate.forEach((rate, tax) -> System.out.printf("%2d%%: Rs %.2f%n", rate, tax));

Practice (0/3 done)

Task 1 · Question

Stream.of("tea", "coffee", "tea", "milk").collect(groupingBy(s -> s, counting())).get("tea") is…

Task 2 · Code · Python

The same grouping in Python, which runs here: write revenue_by_city(sales) where sales is a list of (city, amount) tuples. Return a dict from city to total amount, like groupingBy(city, summingDouble(amount)).

Task 3 · Practice

For a List<Employee>(name, dept, salary), use Collectors to produce: (a) employees per department as a TreeMap<String, List<String>> of names, (b) the average salary per department, (c) a partition of employees earning at least ₹50,000, and (d) one String of all names, sorted, separated by commas.

Quick quiz

Question 1

1.What does groupingBy(Sale::city) produce?

Question 2

2.What does partitioningBy produce?

Question 3

3.toMap with two elements having the same key and no merge function…

Question 4

4.Which collector builds a single String?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What does groupingBy(Sale::city) produce?

Answer: A Map from city to List<Sale>

Grouping builds a map of lists by default.

What does partitioningBy produce?

Answer: Map<Boolean, List<T>>

Keys are true and false.

toMap with two elements having the same key and no merge function…

Answer: Throws IllegalStateException

Provide a merge function to handle duplicates.

Which collector builds a single String?

Answer: joining()

joining concatenates with optional separator, prefix and suffix.

Stream.of(1, 2, 3).map(n -> n * 10).filter(n -> n > 10).toList() is…Related

Answer: [20, 30]

Map to 10, 20, 30, then keep those above 10.

From Streams: filter, map and collect

You merge customers (left) with orders using how="left". What happens to customers with no orders?Related

Answer: They are kept, with missing (NaN) order columns

A left join keeps every row of the left table.

From Grouping, joining and reshaping

For which job is Rust usually NOT the first choice?Related

Answer: A ten-line script to rename some files once

For a quick one-time script, Python or a shell is faster to write. Rust shines when speed and reliability matter.

From What Rust is and why learn it

GROUP BY city, status on data with 3 cities and 2 statuses can give at most how many rows?Related

Answer: 6

One group per different (city, status) pair: up to 3 x 2 = 6. Pairs that never occur do not appear.

From GROUP BY and HAVING

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 1 more topic in “Lambdas, streams and files” before its exam: Reading and writing files.
  2. Module 8 exam: Lambdas, streams and files · 12 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Multithreading and concurrency. 60 topics left in the course before the final exam.

← Previous: Streams: filter, map and collect