CodeGym /Courses /JAVA 25 SELF /Advanced collectors

Advanced collectors

JAVA 25 SELF
Level 31, Lesson 4
Available

1. Introduction

When you work with collections via the Stream API, you often need not just to group elements, but also immediately do something with them: transform, filter, or collect into a different collection type. Java provides downstream collectors — nested collectors applied to each group or partition of data.

What is a downstream collector?

It’s a collector applied to the result of grouping or partitioning. For example, you can group students by course with groupingBy, and within each group collect only names or only honors students (e.g., with a 4.5 GPA threshold).

Examples

mapping

Allows transforming group elements before collecting.

Map<Integer, List<String>> namesByCourse = students.stream()
    .collect(Collectors.groupingBy(
        Student::getCourse,
        Collectors.mapping(Student::getName, Collectors.toList())
    ));
  • We group students by course.
  • For each group we collect names only (not whole objects).

filtering

Allows filtering elements within a group (for example, keep GPA >= 4.5).

Map<Integer, List<Student>> honorsByCourse = students.stream()
    .collect(Collectors.groupingBy(
        Student::getCourse,
        Collectors.filtering(s -> s.getGpa() >= 4.5, Collectors.toList())
    ));

In each group we keep only honors students.

flatMapping

Allows “flattening” nested collections within groups.

Map<String, Set<String>> tagsByAuthor = books.stream()
    .collect(Collectors.groupingBy(
        Book::getAuthor,
        Collectors.flatMapping(
            book -> book.getTags().stream(),
            Collectors.toSet()
        )
    ));

For each author we collect unique tags across all their books.

partitioningBy with downstream

Works similarly to groupingBy, but splits into two groups by a boolean predicate.

Map<Boolean, List<String>> namesByPassed = students.stream()
    .collect(Collectors.partitioningBy(
        s -> s.getGpa() >= 3.0,
        Collectors.mapping(Student::getName, Collectors.toList())
    ));

We split students into those who passed and those who didn’t; within each group — names only.

2. teeing: simultaneous aggregation with two collectors

Sometimes you need to compute several aggregates on a stream at once: for example, both sum and average, or minimum and maximum. For this, Java 12+ introduced the teeing collector.

How does teeing work?

You pass two collectors and a function that combines their results.

Syntax:

Collectors.teeing(collector1, collector2, (result1, result2) -> ...)

Examples

Minimum + maximum

Optional<MinMax> minMax = numbers.stream()
    .collect(Collectors.teeing(
        Collectors.minBy(Integer::compareTo),
        Collectors.maxBy(Integer::compareTo),
        (min, max) -> min.isPresent() && max.isPresent() ? new MinMax(min.get(), max.get()) : null
    ));

We find the minimum and maximum at the same time.

Sum + average

Result result = numbers.stream()
    .collect(Collectors.teeing(
        Collectors.summingInt(Integer::intValue),
        Collectors.averagingInt(Integer::intValue),
        (sum, avg) -> new Result(sum, avg)
    ));

We obtain an object with the sum and the average value.

Example: salary report

SalaryStats stats = employees.stream()
    .collect(Collectors.teeing(
        Collectors.summingInt(Employee::getSalary),
        Collectors.averagingInt(Employee::getSalary),
        SalaryStats::new
    ));

In SalaryStats we store both the sum and the average.

3. toUnmodifiableList/Set/Map and collectingAndThen for “freezing”

Modern Java versions introduced collections that cannot be modified after creation — immutable (unmodifiable). This is convenient for APIs where it’s important that the result can’t be changed accidentally.

toUnmodifiableList/Set/Map

  • Return an immutable collection.
  • Any attempt to add/remove an element will throw UnsupportedOperationException.

Examples:

List<String> names = students.stream()
    .map(Student::getName)
    .collect(Collectors.toUnmodifiableList());
Map<Integer, Student> byId = students.stream()
    .collect(Collectors.toUnmodifiableMap(Student::getId, Function.identity()));

collectingAndThen

Allows applying a function to a collector’s result — for example, to “freeze” the collection.

List<String> names = students.stream()
    .map(Student::getName)
    .collect(Collectors.collectingAndThen(
        Collectors.toList(),
        Collections::unmodifiableList
    ));

First we collect into a regular list, then make it immutable.

Example with Set:

Set<String> tags = books.stream()
    .flatMap(book -> book.getTags().stream())
    .collect(Collectors.collectingAndThen(
        Collectors.toSet(),
        Set::copyOf // Java 10+
    ));

4. Pipeline use cases

Reports and slice-based statistics

With advanced collectors you can build complex reports and statistics “in one line”.

Example: average salary by department

Map<String, Double> avgSalaryByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingInt(Employee::getSalary)
    ));

Example: top 3 most expensive products per category

Map<String, List<Product>> top3ByCategory = products.stream()
    .collect(Collectors.groupingBy(
        Product::getCategory,
        Collectors.collectingAndThen(
            Collectors.toList(),
            list -> list.stream()
                        .sorted(Comparator.comparing(Product::getPrice).reversed())
                        .limit(3)
                        .toList()
        )
    ));

Immutable result as an API contract

If your method returns a collection that cannot be modified, it protects against accidental errors and makes the API more robust.

public List<String> getTags() {
    return tags.stream()
        .collect(Collectors.toUnmodifiableList());
}

The caller won’t be able to do getTags().add("new tag") — an exception will be thrown.

Example: report with multiple aggregates (teeing)

public SalaryReport getSalaryReport(List<Employee> employees) {
    return employees.stream()
        .collect(Collectors.teeing(
            Collectors.averagingInt(Employee::getSalary),
            Collectors.summingInt(Employee::getSalary),
            SalaryReport::new
        ));
}

In SalaryReport we store both the average and the total salary.

5. Common pitfalls and nuances

Error #1: Forgot about immutability. If you return a regular list, it can be modified. Use toUnmodifiableList/Set/Map or collectingAndThen to “freeze” the result.

Error #2: Wrong downstream collector. If you need to transform elements within a group — use mapping; to filter — filtering; to flatten nested collections — flatMapping.

Error #3: UnsupportedOperationException. Occurs when you attempt to modify a collection collected via toUnmodifiableList/Set/Map or “frozen” via collectingAndThen.

Error #4: Lost uniqueness/key collisions. toUnmodifiableSet requires unique elements, and toUnmodifiableMap requires unique keys; otherwise you’ll get an exception during collection.

1
Task
JAVA 25 SELF, level 31, lesson 4
Locked
Sorting Applicants at the Academy of Great Achievements 📜
Sorting Applicants at the Academy of Great Achievements 📜
1
Task
JAVA 25 SELF, level 31, lesson 4
Locked
Express financial portfolio analysis 💰
Express financial portfolio analysis 💰
1
Survey/quiz
Grouping and Aggregation, level 31, lesson 4
Unavailable
Grouping and Aggregation
Stream API: grouping and aggregation
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION