⚡IntermediateFunctional Programming

Stream API

Process collections declaratively using Java Streams: create, transform, filter, and collect sequences of elements.

Overview

The Stream API (Java 8) lets you process sequences of elements in a declarative, pipeline style. Instead of writing loops, you chain operations: filter, map, sort, reduce, and collect. A stream pipeline has three parts: a source (collection, array, or generator), zero or more intermediate operations (lazy, return a new stream), and one terminal operation (triggers evaluation and produces a result or side effect). Streams do not store data; they compute on demand. A stream can only be consumed once.

Key Points

  • •Create streams with collection.stream(), Arrays.stream(arr), Stream.of(...), or Stream.generate()
  • •Intermediate operations are lazy: filter, map, flatMap, sorted, distinct, limit, skip, peek
  • •Terminal operations trigger evaluation: forEach, collect, count, findFirst, anyMatch, allMatch, reduce, min, max
  • •filter(predicate) keeps matching elements
  • •map(function) transforms each element
  • •collect(Collectors.toList()) collects results into a list
  • •Streams are not reusable. Create a new stream for each pipeline
  • •Parallel streams: collection.parallelStream() for multi-threaded processing

Examples

Basic filter, map, and collect

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

public class Main {
    public static void main(String[] args) {
        List<String> names = List.of("Alice","Bob","Charlie","Anna","Dave","Aaron");

        // Find names starting with A, uppercase them, sort, collect to list
        List<String> result = names.stream()
            .filter(s -> s.startsWith("A"))
            .map(String::toUpperCase)
            .sorted()
            .collect(Collectors.toList());

        System.out.println(result); // [AARON, ALICE, ANNA]

        // Count names longer than 4 characters
        long count = names.stream()
            .filter(s -> s.length() > 4)
            .count();
        System.out.println("Long names: " + count); // 2
    }
}

The pipeline is lazy: filter and map are not executed until collect triggers the terminal operation.

Numeric streams and reduction

import java.util.List;
import java.util.OptionalDouble;
import java.util.OptionalInt;

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

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

        OptionalInt max = nums.stream()
                              .mapToInt(Integer::intValue)
                              .max();
        System.out.println("Max: " + max.getAsInt()); // 9

        double avg = nums.stream()
                         .mapToInt(Integer::intValue)
                         .average()
                         .orElse(0);
        System.out.printf("Average: %.2f%n", avg); // 5.00

        // Find first even number greater than 5
        nums.stream()
            .filter(n -> n > 5 && n % 2 == 0)
            .findFirst()
            .ifPresent(n -> System.out.println("Found: " + n)); // 6
    }
}

mapToInt, mapToDouble, mapToLong return primitive streams (IntStream, etc.) that have sum, average, min, max built in.

Collectors: grouping, joining, counting

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

public class Main {
    public static void main(String[] args) {
        List<String> words = List.of("apple","banana","avocado","blueberry","cherry","apricot");

        // Group by first letter
        Map<Character, List<String>> byLetter = words.stream()
            .collect(Collectors.groupingBy(s -> s.charAt(0)));
        System.out.println(byLetter);

        // Count per first letter
        Map<Character, Long> counts = words.stream()
            .collect(Collectors.groupingBy(s -> s.charAt(0), Collectors.counting()));
        System.out.println(counts);

        // Join with separator
        String joined = words.stream()
            .filter(s -> s.startsWith("a"))
            .collect(Collectors.joining(", ", "[", "]"));
        System.out.println(joined); // [apple, avocado, apricot]
    }
}

Collectors.groupingBy is the stream equivalent of SQL GROUP BY. Collectors.joining concatenates strings with a delimiter, optional prefix and suffix.

Common Mistakes

  • Reusing a stream after a terminal operation has been called. This throws IllegalStateException. Create a new stream from the source for each pipeline
  • Mutating data from inside a stream operation (forEach, map). Streams are designed for pure transformations; side effects can cause issues with parallel streams
  • Forgetting the terminal operation: a stream with only intermediate operations does nothing until a terminal operation is called

Note

When you only need to test a condition (does any element match?), prefer anyMatch, allMatch, or noneMatch over collecting to a list and checking size. They short-circuit and are faster.

Exercises

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

Loading editor…

Completed: 0 / 4 exercises

Knowledge Check

Question 1 of 4

What triggers a stream pipeline to actually execute?