Stream Collectors — toList, groupingBy, partitioningBy, mapping, and teeing
In this tutorial, you will learn about Stream Collectors. We cover key concepts, practical examples, and best practices to help you master this topic.
Java stream collectors transform stream elements into various result containers using the Collectors utility class. While terminal operations like count() and forEach() handle simple cases, collectors provide powerful reduction logic — grouping, partitioning, joining, and composing results into maps, sets, and custom data structures.
What You'll Learn
- Basic collectors: toList, toSet, toCollection, joining
- groupingBy: classify by a classifier function
- partitioningBy: split into true/false groups
- mapping and filtering: downstream adapters
- teeing: combine two collectors
Why It Matters
Collectors are the most versatile terminal operation. Mastering them lets you Express complex data aggregations — usually done with nested loops and mutable maps — in a single, readable stream pipeline.
Real-World Use
Report generation groups transactions by month. Analytics tools partition users by active/inactive. Any aggregation dashboard uses groupingBy and summingInt.
Basic Collectors
toList() / toSet()
List<String> names = people.stream()
.map(Person::getName)
.collect(Collectors.toList());
Set<String> uniqueCities = people.stream()
.map(Person::getCity)
.collect(Collectors.toSet());
toCollection()
List<String> names = people.stream()
.map(Person::getName)
.collect(Collectors.toCollection(ArrayList::new));
TreeSet<String> sorted = people.stream()
.map(Person::getName)
.collect(Collectors.toCollection(TreeSet::new));
joining()
String joined = names.stream()
.collect(Collectors.joining(", ", "[", "]"));
// "[Alice, Bob, Charlie]"
summarizingInt/Long/Double
IntSummaryStatistics stats = people.stream()
.collect(Collectors.summarizingInt(Person::getAge));
stats.getCount(); // number of elements
stats.getSum(); // total age
stats.getMin(); // minimum age
stats.getMax(); // maximum age
stats.getAverage(); // average age
groupingBy
The most powerful collector — classifies elements into a Map<K, List<V>>:
Map<String, List<Person>> byCity = people.stream()
.collect(Collectors.groupingBy(Person::getCity));
// {New York=[Alice, Bob], London=[Charlie]}
Downstream Collections
// Count per group
Map<String, Long> countByCity = people.stream()
.collect(Collectors.groupingBy(
Person::getCity,
Collectors.counting()
));
// Average age per city
Map<String, Double> avgAgeByCity = people.stream()
.collect(Collectors.groupingBy(
Person::getCity,
Collectors.averagingInt(Person::getAge)
));
// Sum of ages per city
Map<String, Integer> totalAgeByCity = people.stream()
.collect(Collectors.groupingBy(
Person::getCity,
Collectors.summingInt(Person::getAge)
));
Multi-Level Grouping
Map<String, Map<String, List<Person>>> byCityThenName = people.stream()
.collect(Collectors.groupingBy(
Person::getCity,
Collectors.groupingBy(Person::getName)
));
Custom Map Implementation
Map<String, List<Person>> treeMap = people.stream()
.collect(Collectors.groupingBy(
Person::getCity,
TreeMap::new,
Collectors.toList()
));
partitioningBy
Splits elements into true and false groups:
Map<Boolean, List<Person>> byAge = people.stream()
.collect(Collectors.partitioningBy(p -> p.getAge() >= 18));
List<Person> adults = byAge.get(true);
List<Person> minors = byAge.get(false);
With downstream:
Map<Boolean, Long> countByAge = people.stream()
.collect(Collectors.partitioningBy(
p -> p.getAge() >= 18,
Collectors.counting()
));
mapping and filtering
Adapt downstream collectors by transforming elements:
// Names by city
Map<String, List<String>> namesByCity = people.stream()
.collect(Collectors.groupingBy(
Person::getCity,
Collectors.mapping(Person::getName, Collectors.toList())
));
// Collect only names longer than 3 chars
Map<String, List<String>> filteredNamesByCity = people.stream()
.collect(Collectors.groupingBy(
Person::getCity,
Collectors.filtering(
name -> name.length() > 3,
Collectors.toList()
)
));
teeing (Java 12+)
Combine two independent collectors into one:
record Stats(long count, double average) {}
Stats stats = people.stream()
.collect(Collectors.teeing(
Collectors.counting(),
Collectors.averagingInt(Person::getAge),
Stats::new
));
Another example — min and max in one pass:
record MinMax(int min, int max) {}
MinMax result = IntStream.of(3, 1, 7, 4, 9)
.boxed()
.collect(Collectors.teeing(
Collectors.minBy(Integer::compareTo),
Collectors.maxBy(Integer::compareTo),
(min, max) -> new MinMax(min.orElse(0), max.orElse(0))
));
Custom Collector
For specialized cases, create your own collector:
Collector<Person, StringBuilder, String> nameCollector =
Collector.of(
StringBuilder::new, // supplier
(sb, p) -> sb.append(p.getName()).append(","), // accumulator
(sb1, sb2) -> sb1.append(sb2), // combiner (for parallel)
StringBuilder::toString // finisher
);
String names = people.stream().collect(nameCollector);
Common Mistakes
- Using
toList()fromCollectorsvsstream.toList(). Both return lists, butCollectors.toList()does not guarantee immutability.stream.toList()(Java 16+) returns an immutable list. - Assuming
groupingBypreserves order. It usesHashMapby default, which is unordered. UseLinkedHashMapviagroupingBy(classifier, LinkedHashMap::new, downstream). - Forgetting that
partitioningByalways has two keys. Thefalsekey exists even if no elements match. - Using
filteringincorrectly.filteringis a downstream Adapter for collectors. For stream-level filtering, usestream.filter(). - Over-complicating with custom collectors. 99% of use cases are covered by built-in collectors. Write a custom collector only when necessary.
Practice Questions
1. What is the difference between groupingBy and partitioningBy?
groupingBy classifies by any function and produces Map<K, List<V>>. partitioningBy uses a Predicate and produces Map<Boolean, List<V>> with exactly two keys.
2. What does Collectors.mapping() do?
It adapts a collector to accept a different input type. For example, mapping(Person::getName, toList()) extracts names within a grouping.
3. What is the teeing collector?
It combines two independent collectors into one, producing a single result from both. Introduced in Java 12.
4. How do you ensure groupingBy preserves insertion order?
Use groupingBy(function, LinkedHashMap::new, downstream).
5. What does Collectors.joining(", ") produce?
A String where all stream elements (converted via toString()) are concatenated with ", " as a separator, without prefix or suffix.
Challenge Question:
Write a method Map<String, Double> topAgesByCity(List<Person> people, int topN) that returns the top N oldest ages per city. Use groupingBy with collectingAndThen and toList with sorting and limiting. Also implement a custom collector that computes both sum and count simultaneously without using teeing.
FAQ
Mini Project
Write a program CollectorsDemo.java that:
- Creates a list of 50
Transactionobjects withtype(DEBIT, CREDIT),amount,category, anddate - Groups transactions by type and counts them
- Groups transactions by category and sums the amounts
- Partitions transactions into large (>=100) and small (<100)
- Finds the average amount per category using
averagingDouble - Creates a map of category -> sorted list of amounts (top 3)
- Uses
teeingto compute both total count and total sum in one pass - Joins all unique categories into a comma-separated string
- Collects into an immutable list using
collectingAndThen
What's Next
Streams and collectors handle collections of values, but what about a single value that might be null? Lesson 36 introduces Optional — a container object that may or may not contain a value, providing a functional alternative to null checks.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro