Skip to content

Stream Collectors — toList, groupingBy, partitioningBy, mapping, and teeing

DodaTech Updated 2026-06-28 5 min read

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

  1. Using toList() from Collectors vs stream.toList(). Both return lists, but Collectors.toList() does not guarantee immutability. stream.toList() (Java 16+) returns an immutable list.
  2. Assuming groupingBy preserves order. It uses HashMap by default, which is unordered. Use LinkedHashMap via groupingBy(classifier, LinkedHashMap::new, downstream).
  3. Forgetting that partitioningBy always has two keys. The false key exists even if no elements match.
  4. Using filtering incorrectly. filtering is a downstream Adapter for collectors. For stream-level filtering, use stream.filter().
  5. 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

What is the difference between `collect(Collectors.toList())` and `stream.toList()`?

Both return a list. stream.toList() (Java 16+) guarantees immutability. Collectors.toList() does not — the list may be mutable. Prefer stream.toList() when you want an unmodifiable result.

Can I collect to a specific collection type like TreeSet?

Yes: collect(Collectors.toCollection(TreeSet::new)). This is useful when you need sorted order or custom equality.

What is `collectingAndThen`?

A collector adapter that applies a finisher function after the downstream collect. Example: collectingAndThen(toList(), Collections::unmodifiableList).

Is `groupingBy` concurrent safe?

Yes, groupingByConcurrent is the concurrent variant. It uses ConcurrentHashMap and works with parallel streams.

What is the return type of `groupingBy` without downstream?

Map<K, List<V>>. With a downstream collector, the value type changes (e.g., Map<K, Long> for counting()).

Mini Project

Write a program CollectorsDemo.java that:

  1. Creates a list of 50 Transaction objects with type (DEBIT, CREDIT), amount, category, and date
  2. Groups transactions by type and counts them
  3. Groups transactions by category and sums the amounts
  4. Partitions transactions into large (>=100) and small (<100)
  5. Finds the average amount per category using averagingDouble
  6. Creates a map of category -> sorted list of amounts (top 3)
  7. Uses teeing to compute both total count and total sum in one pass
  8. Joins all unique categories into a comma-separated string
  9. 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