🚀AdvancedAdvanced Streams

Advanced Stream Operations

Master Collectors, groupingBy, flatMap, reduce, and custom collectors for complex data transformations.

Overview

The Stream API has powerful terminal operations beyond simple collect(toList()) and forEach(). Collectors provides factory methods for advanced grouping, partitioning, joining, and statistical operations. You can also build custom Collector implementations for specialized aggregations. The most powerful operations are: - groupingBy: partition elements into groups - flatMap: flatten nested collections - reduce: fold elements into a single result - Collectors.toMap: collect into a Map - Collectors.joining: string concatenation - Custom Collector for multi-accumulation

Key Points

  • •Collectors.groupingBy(classifier) groups elements into Map<K, List<T>>
  • •groupingBy with downstream: Collectors.groupingBy(fn, Collectors.counting())
  • •Collectors.partitioningBy(predicate) splits into Map<Boolean, List<T>>
  • •Collectors.toMap(keyFn, valueFn) builds a Map with a key-value mapping function
  • •Collectors.joining(delimiter, prefix, suffix) concatenates strings
  • •flatMap(fn) maps each element to a stream, then flattens all streams into one
  • •reduce(identity, BinaryOperator) folds the stream into a single value
  • •Collectors.teeing(c1, c2, merger) applies two collectors and merges results (Java 12+)
  • •Custom Collector implements Collector<T, A, R> with supplier, accumulator, combiner, finisher, characteristics

Examples

groupingBy with downstream collectors

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

record Employee(String name, String dept, double salary) {}

public class Main {
    public static void main(String[] args) {
        List<Employee> employees = 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)
        );

        // Group by department, count per group
        Map<String, Long> countByDept = employees.stream()
            .collect(Collectors.groupingBy(Employee::dept, Collectors.counting()));
        System.out.println("Count: " + countByDept);

        // Group by department, average salary per group
        Map<String, Double> avgSalaryByDept = employees.stream()
            .collect(Collectors.groupingBy(Employee::dept, Collectors.averagingDouble(Employee::salary)));
        System.out.println("Avg salary: " + avgSalaryByDept);

        // Group by department, collect names as joined string
        Map<String, String> namesByDept = employees.stream()
            .collect(Collectors.groupingBy(Employee::dept,
                Collectors.mapping(Employee::name, Collectors.joining(", "))));
        System.out.println("Names: " + namesByDept);
    }
}

groupingBy is the Swiss-army-knife for aggregations. The downstream collector determines how each group is summarized: counting, averaging, joining, or collecting into a list.

flatMap for nested collections

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

record Order(String id, List<String> items) {}

public class Main {
    public static void main(String[] args) {
        List<Order> orders = List.of(
            new Order("O1", List.of("apple", "banana")),
            new Order("O2", List.of("cherry", "apple", "mango")),
            new Order("O3", List.of("banana", "grape"))
        );

        // All unique items across all orders
        Set<String> allItems = orders.stream()
            .flatMap(order -> order.items().stream())
            .collect(Collectors.toSet());
        System.out.println("All items: " + allItems);

        // Count how many orders contain each item
        Map<String, Long> itemFrequency = orders.stream()
            .flatMap(order -> order.items().stream())
            .collect(Collectors.groupingBy(item -> item, Collectors.counting()));
        System.out.println("Frequency: " + itemFrequency);
    }
}

flatMap is the key operation for working with nested collections. Each Order's items stream is merged into one flat stream of strings, enabling item-level operations across all orders.

reduce for custom fold operations

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

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5);

        // Sum using reduce
        int sum = numbers.stream().reduce(0, Integer::sum);
        System.out.println("Sum: " + sum); // 15

        // Product
        int product = numbers.stream().reduce(1, (a, b) -> a * b);
        System.out.println("Product: " + product); // 120

        // Max without Comparator
        Optional<Integer> max = numbers.stream().reduce((a, b) -> a > b ? a : b);
        System.out.println("Max: " + max.get()); // 5

        // Build a string by reducing (better done with joining, but illustrates reduce)
        List<String> words = List.of("Hello", "World", "Java");
        String sentence = words.stream().reduce("", (a, b) -> a.isEmpty() ? b : a + " " + b);
        System.out.println(sentence); // Hello World Java
    }
}

reduce with an identity value always returns a value. reduce without identity returns Optional (empty stream = empty Optional). Use reduce for non-standard aggregations not covered by built-in collectors.

Collectors.toMap and partitioningBy

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

record Product(String name, double price, boolean inStock) {}

public class Main {
    public static void main(String[] args) {
        List<Product> products = List.of(
            new Product("Apple",  1.2, true),
            new Product("Banana", 0.5, false),
            new Product("Cherry", 3.0, true),
            new Product("Date",   2.5, false)
        );

        // toMap: name -> price
        Map<String, Double> priceMap = products.stream()
            .collect(Collectors.toMap(Product::name, Product::price));
        System.out.println("Prices: " + priceMap);

        // partitioningBy: in stock vs out of stock
        Map<Boolean, List<Product>> partitioned = products.stream()
            .collect(Collectors.partitioningBy(Product::inStock));
        System.out.println("In stock: " + partitioned.get(true).stream().map(Product::name).toList());
        System.out.println("Out of stock: " + partitioned.get(false).stream().map(Product::name).toList());
    }
}

toMap throws IllegalStateException if two elements map to the same key. Use the three-argument form toMap(keyFn, valueFn, mergeFunction) to handle duplicates. partitioningBy always produces both true and false keys even if one group is empty.

Common Mistakes

  • Using collect(toList()) then forEach instead of using the stream terminal operation directly. Keep pipelines lazy; process in the stream unless you genuinely need the intermediate collection.
  • Calling toMap without a merge function when keys might duplicate. It throws an unchecked IllegalStateException at runtime. Add a merge function: Collectors.toMap(keyFn, valueFn, (v1, v2) -> v1).
  • Using reduce for mutable aggregation (like building a StringBuilder). reduce is for immutable operations. For mutable accumulation, use collect with a custom supplier and accumulator.
  • Mixing sequential and parallel streams without checking thread safety. Collectors like groupingBy and toMap are safe. Custom stateful lambdas in parallel streams are not.

Note

Collectors.teeing (Java 12+) lets you apply two collectors simultaneously in one pass and merge their results. For example, compute both the min and max of a stream in a single iteration: Collectors.teeing(Collectors.minBy(...), Collectors.maxBy(...), List::of).

Exercises

Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.

Loading editor…

Completed: 0 / 3 exercises

Knowledge Check

Question 1 of 4

What does flatMap do differently from map?