Advanced level · Module 1: Generics, Lambdas and Streams · Lesson 4 of 4
Grouping, Summarising and Optional
Group and summarise data with Collectors, and handle possibly missing values with Optional.
In this lesson you will
- Use groupingBy with downstream collectors
- Use Optional instead of returning null
Key concepts
Collectors.groupingBysummingInt / counting / averagingDoubleOptionalorElse / map / ifPresent
The lesson
Collectors.groupingBy(Sale::region) builds a Map from each region to its list of sales. A downstream collector summarises each group instead: groupingBy(Sale::region, summingInt(Sale::amount)) gives the total per region. Wrapping the map in a TreeMap sorts the regions.
Methods that may find nothing should return Optional<T> rather than null. findFirst() and max() on streams already do this. Callers then decide explicitly: orElse(default), orElseThrow(), ifPresent(action) or map to transform the value if it exists.
Do not call get() on an Optional without checking, and do not use Optional for fields or parameters; it is designed for return values.
Step by step
- Group sales by region and count them.
- Change the downstream collector to sum amounts.
- Find the best-selling item with max and Comparator.
- Handle the empty-list case with Optional.orElse.
Sales per region with streams and groupingBy
Group sales by region, total them in sorted order and find the top region — without writing a single loop.
import java.util.List;import java.util.Map;import java.util.TreeMap;import java.util.stream.Collectors; public class RegionReport { record Sale(String region, String item, int amount) {} public static void main(String[] args) { List<Sale> sales = List.of( new Sale("Juba", "rice", 14000), new Sale("Wau", "sugar", 5600), new Sale("Juba", "soap", 4800), new Sale("Malakal", "rice", 7000), new Sale("Wau", "rice", 10500), new Sale("Juba", "sugar", 8400) ); Map<String, Integer> totals = sales.stream() .collect(Collectors.groupingBy(Sale::region, TreeMap::new, Collectors.summingInt(Sale::amount))); totals.forEach((region, total) -> System.out.printf("%-8s %,7d SSP%n", region, total)); String topRegion = totals.entrySet().stream() .max(Map.Entry.comparingByValue()) .map(Map.Entry::getKey) .orElse("none"); System.out.println("Top region: " + topRegion); }}
Output
Juba 27,200 SSP
Malakal 7,000 SSP
Wau 16,100 SSP
Top region: JubaHow it works
groupingBy(Sale::region, TreeMap::new, summingInt(Sale::amount)) builds a map from each region to the sum of its amounts; TreeMap::new keeps the regions in alphabetical order. The second pipeline streams the map's entries, picks the entry with the largest value and maps it to its key. max returns an Optional, so orElse supplies a value for an empty list. Amounts are illustrative training data.
Key concepts
Collectors.groupingBydownstream summingIntTreeMap for sorted keysOptional from max
Practice activity
Produce a regional sales summary with totals per region and the top product per region, handling regions with no sales.
Expected result: The summary matches the expected totals, and a region with no sales prints a clear 'no sales' line instead of failing.
Check your understanding
-
What does groupingBy return?
Show answer
A Map from each key to the grouped results, by default a list of elements.
-
Why return Optional instead of null?
Show answer
It makes the possibility of no result explicit and forces callers to handle it.
Taught live by an instructor — online or in person. Free course lessons are for self-study and do not lead to a certificate — see registration options.