Advanced Stream Operations
Master Collectors, groupingBy, flatMap, reduce, and custom collectors for complex data transformations.
Overview
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.
Completed: 0 / 3 exercises
Knowledge Check
Question 1 of 4