Java Stream API Collectors
Introduced in Java 8, the Stream API fundamentally changed how developers process collections. While streams provide operations like filter and map to transform data, the java.util.stream.Collectors class provides terminal operations that accumulate stream elements into a desired result container, such as a List, Set, Map, or even a single aggregated value.
Accumulating to Collections
The most common use case for a Collector is gathering stream elements back into a standard data structure.
toList() and toSet()
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Alice");
// Collect to a List
List<String> list = names.stream()
.filter(n -> n.length() > 3)
.collect(Collectors.toList());
// Collect to a Set (removes duplicates)
Set<String> set = names.stream()
.collect(Collectors.toSet());
Note: Since Java 16, you can simply use stream.toList() instead of stream.collect(Collectors.toList()) for an unmodifiable list.
toCollection()
If you need a specific collection implementation (e.g., LinkedList or TreeSet), use toCollection:
TreeSet<String> treeSet = names.stream()
.collect(Collectors.toCollection(TreeSet::new));
Collecting to Maps
Converting a stream of objects into a Map is incredibly powerful, especially when indexing or grouping data.
toMap()
List<Employee> employees = getEmployees();
// Map ID to Name
Map<Integer, String> idToName = employees.stream()
.collect(Collectors.toMap(Employee::getId, Employee::getName));
If there are duplicate keys, toMap will throw an IllegalStateException. To handle this, provide a merge function:
Map<String, Integer> nameToSalary = employees.stream()
.collect(Collectors.toMap(
Employee::getName,
Employee::getSalary,
(oldValue, newValue) -> oldValue + newValue // Merge function
));
Grouping and Partitioning
Collectors excel at complex data aggregation, similar to SQL GROUP BY clauses.
groupingBy()
Group elements by a specific property:
// Group employees by department
Map<String, List<Employee>> byDept = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
partitioningBy()
A specialized form of grouping that divides data into exactly two groups (true and false) based on a Predicate:
// Partition into high earners and others
Map<Boolean, List<Employee>> bySalary = employees.stream()
.collect(Collectors.partitioningBy(e -> e.getSalary() > 100000));
Aggregation and Reduction
Collectors can also perform mathematical reductions or string joining.
joining()
Concatenates stream elements into a single String, optionally with a delimiter, prefix, and suffix:
String result = names.stream()
.collect(Collectors.joining(", ", "[", "]"));
// Output: [Alice, Bob, Charlie, Alice]
summarizingInt() / summarizingDouble()
Calculates count, sum, min, average, and max all at once:
IntSummaryStatistics stats = employees.stream()
.collect(Collectors.summarizingInt(Employee::getSalary));
System.out.println("Average: " + stats.getAverage());
System.out.println("Max: " + stats.getMax());
Downstream Collectors
Many collectors (like groupingBy) take a second "downstream" collector to further process the grouped data.
// Group by department, but only count the number of employees in each
Map<String, Long> countByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.counting()
));
// Group by department, but collect their names into a Set
Map<String, Set<String>> namesByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.mapping(Employee::getName, Collectors.toSet())
));
The Collectors class provides a declarative, functional approach to data aggregation in Java, allowing you to write expressive and complex data processing pipelines with minimal code.