14.4 Collectors In Depth
The Collectors class provides collection strategies for the collect() terminal operation. Beyond basic toList() and joining(), learn powerful grouping and aggregation features.
1. Grouping with groupingBy
record Person(String name, int age, String city, double salary) {}
List<Person> people = List.of(
new Person("Alice", 28, "Seoul", 5000),
new Person("Bob", 35, "Busan", 6000),
new Person("Charlie", 28, "Seoul", 7000),
new Person("Dave", 35, "Seoul", 4500),
new Person("Eve", 22, "Daegu", 3500)
);
// Group by city
Map<String, List<Person>> byCity = people.stream()
.collect(Collectors.groupingBy(Person::city));
// Group by city, extract names only (downstream collector)
Map<String, List<String>> namesByCity = people.stream()
.collect(Collectors.groupingBy(
Person::city,
Collectors.mapping(Person::name, Collectors.toList())
));
// Count per city
Map<String, Long> countByCity = people.stream()
.collect(Collectors.groupingBy(Person::city, Collectors.counting()));
// Average salary per city
Map<String, Double> avgSalaryByCity = people.stream()
.collect(Collectors.groupingBy(Person::city,
Collectors.averagingDouble(Person::salary)));
Multi-level Grouping
Map<String, Map<Integer, List<Person>>> byCityAndAge = people.stream()
.collect(Collectors.groupingBy(Person::city,
Collectors.groupingBy(Person::age)));
2. partitioningBy - Split Into Two Groups
Map<Boolean, List<Person>> partitioned = people.stream()
.collect(Collectors.partitioningBy(p -> p.salary() >= 5000));
System.out.println("High salary: " + partitioned.get(true).stream()
.map(Person::name).collect(Collectors.joining(", ")));
// High salary: Alice, Bob, Charlie
// Prime vs composite numbers
List<Integer> nums = IntStream.rangeClosed(2, 20).boxed().toList();
Map<Boolean, List<Integer>> primes = nums.stream()
.collect(Collectors.partitioningBy(n -> {
for (int i = 2; i * i <= n; i++) if (n % i == 0) return false;
return true;
}));
System.out.println("Primes: " + primes.get(true)); // [2, 3, 5, 7, 11, 13, 17, 19]
3. toMap - Collect to Map
Map<String, Double> nameSalaryMap = people.stream()
.collect(Collectors.toMap(Person::name, Person::salary));
// Handle key collision
Map<String, Double> avgSalaryMap = people.stream()
.collect(Collectors.toMap(
Person::city,
Person::salary,
(existing, replacement) -> (existing + replacement) / 2
));
// Preserve insertion order (LinkedHashMap)
Map<String, Double> orderedMap = people.stream()
.collect(Collectors.toMap(
Person::name, Person::salary,
(a, b) -> a,
LinkedHashMap::new
));
4. Statistics Collectors
DoubleSummaryStatistics stats = people.stream()
.collect(Collectors.summarizingDouble(Person::salary));
System.out.println("Count: " + stats.getCount());
System.out.println("Sum: " + stats.getSum());
System.out.println("Average: " + stats.getAverage());
System.out.println("Min: " + stats.getMin());
System.out.println("Max: " + stats.getMax());
5. Other Key Collectors
// joining
String joined = people.stream()
.map(Person::name)
.collect(Collectors.joining(", ", "[", "]"));
// [Alice, Bob, Charlie, Dave, Eve]
// counting
long count = people.stream().collect(Collectors.counting()); // 5
// summingInt / averagingInt
int total = people.stream()
.collect(Collectors.summingInt(p -> (int) p.salary()));
Pro Tip
Sorted grouping result:
Map<String, Long> sorted = people.stream()
.collect(Collectors.groupingBy(
Person::city,
TreeMap::new, // Keys auto-sorted
Collectors.counting()
));
Collectors.teeing (Java 12+): Process a stream with two collectors simultaneously.
record MinMax(double min, double max) {}
MinMax result = people.stream()
.collect(Collectors.teeing(
Collectors.minBy(Comparator.comparingDouble(Person::salary)),
Collectors.maxBy(Comparator.comparingDouble(Person::salary)),
(min, max) -> new MinMax(
min.map(Person::salary).orElse(0.0),
max.map(Person::salary).orElse(0.0)
)
));
System.out.println(result); // MinMax[min=3500.0, max=7000.0]