44. Streams — Collectors
Collectors provides ready-made terminal collectors: groupingBy, partitioningBy, joining, toUnmodifiableList, and counting — and you can compose them for multi-level aggregation.
Explanation
Collectors.groupingBy(classifier) groups elements into a Map<K, List<V>>. The downstream can be another collector: groupingBy(key, counting()) gives a Map<K, Long>, and groupingBy(key, groupingBy(key2)) gives nested maps.
partitioningBy(predicate) is a specialised groupingBy that always produces a Map<Boolean, List<T>> with exactly two groups. joining(delimiter, prefix, suffix) concatenates strings.
Java 16 added Stream.toList() — a convenience terminal that returns an unmodifiable List<T> (equivalent to collect(Collectors.toUnmodifiableList())). This is preferred over collect(Collectors.toList()) (which returns a mutable list) for read-only results.
Example
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class StreamsCollectors {
record Employee(String name, String dept, double salary) {}
public static void main(String[] args) {
var employees = List.of(
new Employee("Alice", "Engineering", 95_000),
new Employee("Bob", "Engineering", 85_000),
new Employee("Carol", "Marketing", 70_000),
new Employee("Dave", "Marketing", 65_000),
new Employee("Eve", "Engineering", 105_000)
);
// groupingBy with downstream counting
Map<String, Long> countByDept = employees.stream()
.collect(Collectors.groupingBy(Employee::dept, Collectors.counting()));
System.out.println("Count by dept: " + countByDept);
// groupingBy with downstream averaging
Map<String, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(Employee::dept,
Collectors.averagingDouble(Employee::salary)));
avgSalaryByDept.forEach((dept, avg) ->
System.out.printf("%s: $%.0f avg%n", dept, avg));
// partitioningBy: above/below 80k
Map<Boolean, List<Employee>> partition = employees.stream()
.collect(Collectors.partitioningBy(e -> e.salary() >= 80_000));
System.out.println("Senior (>=80k): " + partition.get(true).stream().map(Employee::name).toList());
System.out.println("Junior (<80k): " + partition.get(false).stream().map(Employee::name).toList());
// joining
String names = employees.stream()
.map(Employee::name)
.collect(Collectors.joining(", ", "[", "]"));
System.out.println("Names: " + names);
// toList() (Java 16+) — unmodifiable
List<String> engineerNames = employees.stream()
.filter(e -> "Engineering".equals(e.dept()))
.map(Employee::name)
.toList(); // unmodifiable!
System.out.println("Engineers: " + engineerNames);
}
}Sources
- https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/stream/Collectors.html — Collectors API
- https://openjdk.org/jeps/397 — JEP 397: Stream.toList() context (Java 16)