Modern Java

43. Streams — Basics

Stream<T> is a lazy sequence of elements supporting pipeline-style operations — filter, map, flatMap, collect, reduce — that are evaluated only when a terminal operation is invoked.

Java 8+ (streams); Java 21 (core language)

Explanation

A stream pipeline has three parts: a source (collection, array, generator, I/O channel), zero or more intermediate operations (lazy — they return a new stream and don't execute until a terminal is called), and a terminal operation (eager — triggers evaluation and produces a result or side effect).

Key intermediate operations: filter(pred) keeps matching elements; map(fn) transforms each element; flatMap(fn) maps to a stream and flattens; sorted() / sorted(comparator) sorts; distinct() removes duplicates; peek(consumer) for debugging (don't use for side effects in production); limit(n) / skip(n) for windowing.

Key terminal operations: collect(collector) gathers into a collection; reduce(identity, accumulator) folds; forEach(consumer) side effects; count(), min(), max(), findFirst(), anyMatch(pred), allMatch(pred), noneMatch(pred).

Streams are not reusable — a stream can be consumed only once. Attempting to use it again throws IllegalStateException.

Example

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class StreamsBasics {
    record Product(String name, String category, double price) {}

    public static void main(String[] args) {
        var products = List.of(
            new Product("Laptop",  "Electronics", 999.99),
            new Product("Phone",   "Electronics", 699.00),
            new Product("Desk",    "Furniture",   299.50),
            new Product("Chair",   "Furniture",   199.00),
            new Product("Monitor", "Electronics", 349.99)
        );

        // filter + map + collect
        List<String> expensiveElectronics = products.stream()
            .filter(p -> "Electronics".equals(p.category()))
            .filter(p -> p.price() > 500)
            .map(Product::name)
            .sorted()
            .collect(Collectors.toList());
        System.out.println("Expensive electronics: " + expensiveElectronics);

        // reduce: total price
        double total = products.stream()
            .mapToDouble(Product::price)
            .sum(); // specialised: avoids boxing
        System.out.printf("Total: $%.2f%n", total);

        // flatMap: flatten lists
        List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3, 4), List.of(5));
        List<Integer> flat = nested.stream().flatMap(List::stream).collect(Collectors.toList());
        System.out.println("Flat: " + flat);

        // Terminal: count, min, max, findFirst
        long count = products.stream().filter(p -> p.price() < 400).count();
        System.out.println("Cheap products: " + count);

        products.stream()
                .max(java.util.Comparator.comparingDouble(Product::price))
                .ifPresent(p -> System.out.println("Most expensive: " + p.name()));

        // groupingBy
        Map<String, List<Product>> byCategory = products.stream()
            .collect(Collectors.groupingBy(Product::category));
        byCategory.forEach((cat, ps) ->
            System.out.println(cat + ": " + ps.stream().map(Product::name).toList()));
    }
}

Sources