Use toList, groupingBy, partitioningBy, joining, counting, and custom collectors to aggregate stream data.
Published February 21, 2025
Collectors is a factory class providing implementations of Collector — the terminal operation that reduces a stream into a result container. Beyond simple toList(), collectors unlock grouping, partitioning, statistics, and string joining.
List<String> names = stream.collect(Collectors.toList());
Set<String> nameSet = stream.collect(Collectors.toSet());
String csv = stream.collect(Collectors.joining(", "));
String delimited = stream.collect(Collectors.joining(", ", "[", "]"));
long count = stream.collect(Collectors.counting());
List<Person> people = ...;
// Group by one field
Map<String, List<Person>> byCity =
people.stream().collect(Collectors.groupingBy(Person::getCity));
// Group and count (downstream collector)
Map<String, Long> countByCity =
people.stream().collect(
Collectors.groupingBy(Person::getCity, Collectors.counting()));
// Group and collect to specific collection type
Map<String, Set<String>> emailsByCity =
people.stream().collect(
Collectors.groupingBy(Person::getCity,
Collectors.mapping(Person::getEmail, Collectors.toSet())));
// Multi-level grouping
Map<String, Map<String, List<Person>>> byCityThenGender =
people.stream().collect(
Collectors.groupingBy(Person::getCity,
Collectors.groupingBy(Person::getGender)));
// Split into adults and minors
Map<Boolean, List<Person>> partitioned =
people.stream().collect(
Collectors.partitioningBy(p -> p.getAge() >= 18));
List<Person> adults = partitioned.get(true);
List<Person> minors = partitioned.get(false);
// Statistics in one pass
IntSummaryStatistics stats = people.stream()
.collect(Collectors.summarizingInt(Person::getAge));
stats.getCount(); // 100
stats.getSum(); // 3500
stats.getMin(); // 18
stats.getMax(); // 75
stats.getAverage(); // 35.0
// Simpler aggregations
Optional<Integer> max = people.stream()
.collect(Collectors.maxBy(Comparator.comparing(Person::getAge)));
Double average = people.stream()
.collect(Collectors.averagingInt(Person::getAge));
Integer sum = people.stream()
.collect(Collectors.summingInt(Person::getSalary));
// toMap(keyMapper, valueMapper)
Map<String, Person> byEmail =
people.stream().collect(
Collectors.toMap(Person::getEmail, p -> p));
// With merge function for duplicate keys
Map<String, String> cityToFirstName =
people.stream().collect(
Collectors.toMap(
Person::getCity,
Person::getName,
(existing, replacement) -> existing // keep first
));
record Summary(long count, double average) {}
Summary summary = people.stream().collect(
Collectors.teeing(
Collectors.counting(),
Collectors.averagingInt(Person::getAge),
Summary::new
)
);
List<String> immutable = stream.collect(Collectors.toUnmodifiableList());
Set<String> immutableSet = stream.collect(Collectors.toUnmodifiableSet());
groupingBy with a downstream collector is a common interview question — know how to group and then aggregate (count, sum, join).partitioningBy returns exactly a Map<Boolean, List<T>> — the map always has both true and false keys.Collectors.toList() returns a mutable list; Stream.toList() (Java 16+) returns an unmodifiable list — they differ in mutability.