Intermediate · Lambdas, streams and files · topic 39 of 60 · 25 min
Collectors: grouping and summarising
Explanation
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.
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 cityMore collectors
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.0toMap and the duplicate-key trap
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 priceCollectors 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.
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}}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
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.
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)
Stream.of("tea", "coffee", "tea", "milk").collect(groupingBy(s -> s, counting())).get("tea") is…
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)).
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
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Reading and writing filesLambdas, streams and files · same module
- Method referencesLambdas, streams and files · same module
- Grouping, joining and reshapingData Science, Machine Learning and AI · Cleaning, exploring and visualising data
- What Rust is and why learn itRust Programming · Getting started with Rust
- GROUP BY and HAVINGBackend Development · SQL in depth
- Go language interview questionsGo Programming (backend and cloud) · Careers and interviews
Questions people ask about this
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
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.
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.
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 more topic in “Lambdas, streams and files” before its exam: Reading and writing files.
- Module 8 exam: Lambdas, streams and files · 12 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Multithreading and concurrency. 60 topics left in the course before the final exam.