Downstream Collectors

Streams · lesson 32 of 42 · 4 min read

filtering, flatMapping, reducing and collectingAndThen: the second tier of Collectors.

Open this lesson in the learning hub

Key points

  • Collectors.filtering (Java 9+) filters inside a group, so a group that empties out still exists.
  • Filtering before groupingBy instead makes that group vanish. Which one you want is a real decision.
  • flatMapping flattens per group: orders to line items, teams to members, in a single pass.
  • reducing folds a group to one value when counting and summingInt are not enough.
  • collectingAndThen bolts a finishing step on: freeze the list, unwrap the Optional, format the number.
  • They nest as deep as you like, but two levels is usually where a reader gives up.

Example

import java.util.*;
import java.util.stream.*;

public class Main {
    record Order(String customer, List<String> items, int total) {}

    public static void main(String[] args) {
        List<Order> orders = List.of(
                new Order("ada", List.of("pen", "ink"), 40),
                new Order("ada", List.of("pad"), 5),
                new Order("bo", List.of("mug"), 8));

        // filtering: the group survives even when nothing in it passes.
        Map<String, List<Order>> inside = orders.stream().collect(Collectors.groupingBy(
                Order::customer, TreeMap::new,
                Collectors.filtering(o -> o.total() > 10, Collectors.toList())));

        // Filtering before the grouping instead: bo disappears completely.
        Map<String, List<Order>> outside = orders.stream().filter(o -> o.total() > 10)
                .collect(Collectors.groupingBy(Order::customer, TreeMap::new, Collectors.toList()));

        System.out.println("filtering() keys : " + inside.keySet());
        System.out.println("filter-first keys: " + outside.keySet());
        System.out.println("bo, inside       : " + inside.get("bo"));

        // flatMapping: every item each customer bought, in one pass.
        Map<String, List<String>> items = orders.stream().collect(Collectors.groupingBy(
                Order::customer, TreeMap::new,
                Collectors.flatMapping(o -> o.items().stream(), Collectors.toList())));
        System.out.println("flatMapping      : " + items);

        // reducing folds a group; collectingAndThen finishes the whole result.
        Map<String, Integer> spend = orders.stream().collect(Collectors.groupingBy(
                Order::customer, TreeMap::new, Collectors.reducing(0, Order::total, Integer::sum)));
        List<String> frozen = orders.stream().collect(Collectors.collectingAndThen(
                Collectors.mapping(Order::customer, Collectors.toList()), List::copyOf));
        System.out.println("reducing         : " + spend);
        System.out.println("collectingAndThen: " + frozen);
    }
}

Filtering inside a group keeps the key; filtering before it deletes the key.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Streams course, and every lesson in it is listed on the Streams contents page.