groupingBy hands each group to a downstream collector; collectingAndThen lets that downstream finish with a sort and a limit. One pass builds the groups, and each group is ranked as it closes.
record Sale(String region, String rep, double amount) {}
var sales = List.of(new Sale("east", "Ava", 300.0), new Sale("east", "Ben", 500.0),
new Sale("east", "Cara", 100.0), new Sale("west", "Dan", 250.0),
new Sale("west", "Eve", 900.0), new Sale("west", "Fay", 400.0));
Map<String, List<String>> top2 = sales.stream().collect(Collectors.groupingBy(Sale::region, TreeMap::new,
Collectors.collectingAndThen(Collectors.toList(), group -> group.stream()
.sorted(Comparator.comparingDouble(Sale::amount).reversed())
.limit(2)
.map(s -> s.rep() + "(" + s.amount() + ")")
.toList())));
top2.forEach((region, reps) -> System.out.println(region + ": " + reps));
east: [Ben(500.0), Ava(300.0)]
west: [Eve(900.0), Fay(400.0)]
Run this yourself in the Online Java Compiler, spin up a live REST API in the API Sandbox, or practise with Java interview questions.
Published 2026-09-20