Stream Collectors
IntermediateUse Collectors to accumulate stream elements into lists, maps, grouped structures, joined strings, and custom containers.
Overview
The Collectors utility class provides factory methods for the most common reduction strategies. collect(Collectors.toList()) is the most-used terminal operation, but Collectors also supports groupingBy (build a Map<K,List<V>>), partitioningBy (split into true/false), joining (concatenate strings), counting, summing, averaging, toMap, toUnmodifiableList, and teeing (Java 12). Downstream collectors let you chain operations: groupingBy then counting, groupingBy then mapping, etc.
toList, toSet, toMap, joining
toList(), toSet(), toUnmodifiableList() collect elements into standard collections. toMap requires a key extractor and a value extractor; if keys can collide, provide a merge function as the third argument. joining() concatenates strings with optional delimiter, prefix, and suffix.
import java.util.*;
import java.util.stream.*;
public class BasicCollectors {
record Person(String name, String city, int age) {}
public static void main(String[] args) {
List<Person> people = List.of(
new Person("Alice", "NYC", 30),
new Person("Bob", "LA", 25),
new Person("Carol", "NYC", 35),
new Person("Dave", "LA", 28)
);
// toList (Java 16 shorthand), toSet, toUnmodifiableList
List<String> names = people.stream()
.map(Person::name)
.collect(Collectors.toList());
System.out.println(names);
// toMap — key must be unique or merge function required
Map<String, Integer> nameToAge = people.stream()
.collect(Collectors.toMap(Person::name, Person::age));
System.out.println(nameToAge);
// toMap with merge function for duplicate keys
Map<String, Long> cityCount = people.stream()
.collect(Collectors.toMap(
Person::city,
p -> 1L,
Long::sum));
System.out.println(cityCount); // {NYC=2, LA=2}
// joining — concatenate strings
String csv = people.stream()
.map(Person::name)
.collect(Collectors.joining(", ", "[", "]"));
System.out.println(csv); // [Alice, Bob, Carol, Dave]
// counting, summingInt, averagingInt
long total = people.stream().collect(Collectors.counting());
int sumAge = people.stream().collect(Collectors.summingInt(Person::age));
double avg = people.stream().collect(Collectors.averagingInt(Person::age));
System.out.println(total + " | " + sumAge + " | " + avg); // 4 | 118 | 29.5
}
}groupingBy & partitioningBy
groupingBy(classifier) produces Map<K, List<V>>. A downstream collector as the second argument transforms the value list: counting(), mapping(), toSet(), joining(), summarizingInt(), etc.
partitioningBy(Predicate) is a specialised groupingBy with boolean keys — always produces a Map<Boolean, List<T>> with both true and false keys present.
import java.util.*;
import java.util.stream.*;
public class GroupingCollectors {
record Employee(String name, String dept, double salary) {}
public static void main(String[] args) {
List<Employee> emps = List.of(
new Employee("Alice", "Engineering", 95000),
new Employee("Bob", "Engineering", 88000),
new Employee("Carol", "Marketing", 72000),
new Employee("Dave", "Marketing", 68000),
new Employee("Eve", "Engineering", 102000)
);
// groupingBy — Map<dept, List<Employee>>
Map<String, List<Employee>> byDept =
emps.stream().collect(Collectors.groupingBy(Employee::dept));
byDept.forEach((d, list) ->
System.out.println(d + ": " + list.stream().map(Employee::name).toList()));
// groupingBy + counting downstream
Map<String, Long> countByDept =
emps.stream().collect(Collectors.groupingBy(Employee::dept, Collectors.counting()));
System.out.println(countByDept); // {Engineering=3, Marketing=2}
// groupingBy + averagingDouble downstream
Map<String, Double> avgSalary = emps.stream().collect(
Collectors.groupingBy(Employee::dept, Collectors.averagingDouble(Employee::salary)));
System.out.println(avgSalary);
// groupingBy + mapping downstream — Map<dept, List<name>>
Map<String, List<String>> namesByDept = emps.stream().collect(
Collectors.groupingBy(Employee::dept,
Collectors.mapping(Employee::name, Collectors.toList())));
System.out.println(namesByDept);
// partitioningBy — split into true/false
Map<Boolean, List<Employee>> highEarners = emps.stream()
.collect(Collectors.partitioningBy(e -> e.salary() > 80000));
System.out.println("High: " + highEarners.get(true).stream().map(Employee::name).toList());
System.out.println("Low: " + highEarners.get(false).stream().map(Employee::name).toList());
}
}teeing, toUnmodifiable & Custom Collectors
Collectors.teeing (Java 12) runs two collectors in parallel on the same stream and merges their results — useful when you need two aggregations in one pass (e.g., min and max simultaneously).
Collectors.toUnmodifiableList/Set/Map return immutable views. For full custom collectors, implement the Collector<T,A,R> interface with supplier, accumulator, combiner, finisher, and characteristics.
import java.util.*;
import java.util.stream.*;
import java.util.function.Function;
public class AdvancedCollectors {
public static void main(String[] args) {
List<Integer> nums = List.of(3, 1, 4, 1, 5, 9, 2, 6);
// teeing (Java 12) — two collectors, one merge
record MinMax(int min, int max) {}
MinMax minmax = nums.stream().collect(
Collectors.teeing(
Collectors.minBy(Integer::compareTo),
Collectors.maxBy(Integer::compareTo),
(min, max) -> new MinMax(min.orElseThrow(), max.orElseThrow())
));
System.out.println(minmax); // MinMax[min=1, max=9]
// toUnmodifiableList — immutable result
List<Integer> immutable = nums.stream()
.filter(n -> n > 3)
.collect(Collectors.toUnmodifiableList());
// immutable.add(1); // UnsupportedOperationException
// Frequency map using groupingBy + counting
Map<Integer, Long> freq = nums.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
System.out.println(freq); // {1=2, 2=1, 3=1, 4=1, 5=1, 6=1, 9=1}
// summarizingInt — get count/sum/min/max/average in one pass
IntSummaryStatistics stats = nums.stream()
.collect(Collectors.summarizingInt(Integer::intValue));
System.out.println(stats);
// IntSummaryStatistics{count=8, sum=31, min=1, average=3.875, max=9}
}
}Interactive Visualization
stream.filter(n → n%2==0).map(n → n*n).sorted().collect(toList())Key Points to Remember
- groupingBy produces Map<K, List<V>>; add a downstream collector to transform the values
- partitioningBy always produces Map<Boolean, List<T>> — both true and false keys exist
- joining(delimiter, prefix, suffix) is the clean way to build CSV/bracketed strings
- toMap with three args (key, value, mergeFunction) handles duplicate keys gracefully
- teeing (Java 12) runs two collectors in one pass — great for simultaneous min/max
- summarizingInt/Long/Double returns count, sum, min, max, and average in one collector
Practice Stream Collectors in the Playground
Run and modify code directly in your browser - no setup needed.
Interview Questions
Sign in to ask AriaWhat does Collectors.groupingBy() return?
What is the difference between groupingBy and partitioningBy?
How do you handle duplicate keys in Collectors.toMap()?
How would you count the frequency of each element in a list using Streams?
What does Collectors.teeing() do?
Ask Aria about Stream Collectors
Your personal AI tutor — ask anything about this concept