Intermediate Operations

Basic Java
≈ 2 min read

Intermediate operations build a pipeline

An intermediate operation receives a stream and returns a stream. It does not produce the final result and does not start processing by itself. That is why these operations are convenient to connect into a chain.

filter and map: selection and transformation

filter keeps elements for which a Predicate returns true. map turns each element into another value.

List<String> result = List.of("java", "go", "spring").stream()
        .filter(word -> word.length() >= 4)
        .map(String::toUpperCase)
        .toList();

Short strings are removed first, and the remaining ones are then converted to uppercase. The order of steps matters: if the operations are swapped, the result may sometimes stay the same, but the amount and meaning of the work performed can change.

flatMap unfolds nested sequences

A regular map creates one result for each input element. If each element itself turns into several elements, flatMap is often needed: it combines the resulting inner streams into one common stream.

List<List<String>> groups = List.of(
        List.of("A", "B"),
        List.of("C", "D")
);

List<String> letters = groups.stream()
        .flatMap(List::stream)
        .toList();

The result contains A, B, C, D, not two nested lists.

distinct, sorted, and limiting a sequence

distinct() removes duplicate elements, sorted() sorts them, limit(n) keeps no more than the first n elements, and skip(n) skips the first n. Ordered streams also provide takeWhile and dropWhile, which work with the initial part of a sequence according to a condition.

List<Integer> values = List.of(4, 2, 4, 1, 3).stream()
        .distinct()
        .sorted()
        .limit(3)
        .toList();

The result here is [1, 2, 3].

peek is not a replacement for normal logic

peek lets you perform an action on an element as it passes through the pipeline. It is convenient for brief diagnostics, but important changes to external state should not be built around peek. A stream may optimize the pipeline, and laziness makes the exact moment of execution less obvious.

Intermediate operations are best kept free of side effects. Let a lambda compute its result only from its arguments and avoid modifying the stream source. Such a pipeline is easier to read and safer to execute sequentially or in parallel.