8.2 Intermediate Stream Operations: Filtering, Mapping, Sorting, and Slicing

Key Takeaways

  • Intermediate operations return a new stream instance and are classified as either stateless (processing elements independently, e.g., filter, map, flatMap) or stateful (retaining historical state, e.g., distinct, sorted, limit, skip).

  • map(Function) performs 1-to-1 element transformation, whereas flatMap(Function) performs 1-to-many transformation by flattening nested streams into a single composite stream.

  • distinct() removes duplicate elements using Object.equals() and hashCode(), while sorted() orders elements by Comparable natural ordering or an explicit Comparator.

  • takeWhile(Predicate) short-circuits upon the first false predicate evaluation, whereas dropWhile(Predicate) discards elements until the first false predicate evaluation and then emits all remaining elements.

  • peek(Consumer) is intended exclusively for non-intrusive debugging; modifying external state inside peek violates non-interference, and the JVM may elide peek calls when stream sizes are pre-calculated.

Last updated: October 2026

8.2 Intermediate Stream Operations: Filtering, Mapping, Sorting, and Slicing

Intermediate operations form the transformation stages of a stream pipeline. Every intermediate operation transforms an input stream into a new output stream, enabling declarative method chaining. Because intermediate operations are lazily evaluated, they act as descriptive blueprints for data manipulation. Java categorizes intermediate operations into stateless and stateful operations based on their internal memory requirements and parallel processing semantics.


Stateless vs. Stateful Intermediate Operations

Understanding the distinction between stateless and stateful operations is critical for performance tuning and predicting pipeline behavior:

  • Stateless Operations: Each element is processed independently without retaining state or knowledge of previously evaluated elements. Stateless operations require O(1)O(1) auxiliary memory and parallelize effortlessly across CPU cores.
    • Examples: filter, map, mapToInt, flatMap, peek.
  • Stateful Operations: Processing an element requires retaining historical state from previously encountered elements or buffering the entire stream before yielding downstream elements. Stateful operations may need to buffer elements (sorted and distinct can hold the whole stream, roughly O(n)O(n) memory) or track position (limit, skip), and they add coordination cost in parallel streams.
    • Examples: distinct, sorted, limit, skip, takeWhile, dropWhile.

Warning

The Stateful Sorting Barrier on Infinite Streams The sorted() operation is stateful and non-short-circuiting: it cannot emit a single sorted element until it has examined and buffered every element from upstream. Invoking sorted() on an infinite stream (e.g., Stream.iterate(1, n -> n + 1).sorted().limit(5)) creates an infinite loop that eventually crashes the JVM with OutOfMemoryError: Java heap space. To sort an infinite stream, the short-circuiting operation must precede sorting: .limit(5).sorted().


Filtering and Element Transformation

filter(Predicate<? super T>)

The filter operation evaluates each upstream element against a Predicate. If the predicate returns true, the element passes downstream; if false, the element is dropped:

List<String> names = List.of("Brian", "Alice", "Bob", "Alexander");
List<String> longNames = names.stream()
    .filter(s -> s.length() > 5)
    .toList(); // [Alexander]

map(Function<? super T, ? extends R>)

The map operation performs a one-to-one transformation. For every input element of type T, it applies the Function and emits exactly one element of type R downstream:

List<String> words = List.of("Java", "Stream", "API");
List<Integer> lengths = words.stream()
    .map(String::length) // Transforms Stream<String> into Stream<Integer>
    .toList(); // [4, 6, 3]

flatMap(Function<? super T, ? extends Stream<? extends R>>)

The flatMap operation performs a one-to-many transformation. Each input element is mapped to an independent Stream<R>, and flatMap flattens all of these individual streams into a single, unified Stream<R>:

List<List<String>> nested = List.of(
    List.of("Spring", "Hibernate"),
    List.of("Micronaut", "Quarkus"),
    List.of("Helidon")
);

// Flattening nested collections
List<String> frameworks = nested.stream()
    .flatMap(Collection::stream) // Flattens Stream<List<String>> into Stream<String>
    .map(String::toUpperCase)
    .toList(); 
// [SPRING, HIBERNATE, MICRONAUT, QUARKUS, HELIDON]

flatMap is indispensable when unwrapping complex structures, such as splitting sentences into words (sentence -> Arrays.stream(sentence.split(" "))) or unpacking nested Optional references.


Duplicate Elimination and Sorting

distinct()

The distinct() method removes duplicate elements from the stream. Uniqueness is determined strictly according to Object.equals(Object) and Object.hashCode():

List<Integer> nums = List.of(1, 2, 2, 3, 3, 3, 4);
List<Integer> unique = nums.stream().distinct().toList(); // [1, 2, 3, 4]

For custom domain classes, failing to properly override both equals() and hashCode() prevents distinct() from identifying duplicate objects.

sorted() and sorted(Comparator<? super T>)

Java provides two sorting operations:

  • sorted(): Sorts elements according to their natural ordering. Elements must implement Comparable<? super T>.
  • sorted(Comparator<? super T>): Sorts elements according to an explicit Comparator.
List<String> cities = List.of("Paris", "Tokyo", "Berlin", "Cairo");

// Natural order (alphabetical)
List<String> natural = cities.stream().sorted().toList();
// [Berlin, Cairo, Paris, Tokyo]

// Custom comparator (descending length)
List<String> byLengthDesc = cities.stream()
    .sorted(Comparator.comparingInt(String::length).reversed())
    .toList();
// [Berlin, Paris, Tokyo, Cairo]

Warning

The ClassCastException on sorted() Due to generic type erasure, calling the no-argument sorted() method on a stream of objects that do not implement Comparable compiles without error. However, when the terminal operation executes, the JVM attempts to cast the elements to Comparable, throwing a runtime ClassCastException: class CustomType cannot be cast to class java.lang.Comparable.


Stream Slicing Operations

limit(long maxSize) and skip(long n)

  • limit(maxSize): Truncates the stream to at most maxSize elements. It is a short-circuiting stateful operation.
  • skip(n): Discards the first n elements and emits all subsequent elements.
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> middle = numbers.stream()
    .skip(3)  // Discards 1, 2, 3
    .limit(4) // Retains 4, 5, 6, 7
    .toList();

Java 9 Slicing: takeWhile(Predicate) vs. dropWhile(Predicate)

Introduced in Java 9, takeWhile and dropWhile provide conditional slicing based on predicate evaluation along the stream's encounter order:

  • takeWhile(Predicate<? super T>): Takes elements from the beginning of the stream as long as the predicate evaluates to true. The moment an element evaluates to false, takeWhile immediately terminates and short-circuits, dropping that element and all remaining elements regardless of whether later elements satisfy the predicate.
  • dropWhile(Predicate<? super T>): Discards elements from the beginning of the stream as long as the predicate evaluates to true. The moment an element evaluates to false, dropWhile stops dropping and emits that element and all remaining downstream elements without testing the predicate ever again.
List<Integer> values = List.of(2, 4, 6, 7, 8, 10, 12);

// takeWhile stops at first odd number (7)
List<Integer> taken = values.stream()
    .takeWhile(n -> n % 2 == 0)
    .toList(); // [2, 4, 6]

// dropWhile drops until first odd number (7), then emits everything else
List<Integer> dropped = values.stream()
    .dropWhile(n -> n % 2 == 0)
    .toList(); // [7, 8, 10, 12]
MethodBehavior on Ordered StreamShort-Circuiting?
filter(p)Evaluates all elements; keeps every element matching p.No
takeWhile(p)Takes contiguous matching prefix; stops at first non-matching element.Yes
dropWhile(p)Drops contiguous matching prefix; emits first non-match and all subsequent elements.No

Note

On unordered streams (e.g., from HashSet.stream()), takeWhile and dropWhile are non-deterministic: the subset of elements taken or dropped depends on arbitrary iteration order.


Side-Effect Inspection with peek(Consumer<? super T>)

The peek operation applies a Consumer to each element as it passes through the pipeline, returning an identical stream. It is designed strictly for diagnostic logging and debugging:

List<String> result = Stream.of("one", "two", "three", "four")
    .filter(e -> e.length() > 3)
    .peek(e -> System.out.println("Filtered value: " + e))
    .map(String::toUpperCase)
    .peek(e -> System.out.println("Mapped value: " + e))
    .toList();

The peek Anti-Pattern and Optimization Caveat

Using peek to mutate program state or execute business logic is a dangerous anti-pattern:

  1. Non-interference violation: Modifying shared mutable state inside peek causes race conditions in parallel pipelines.
  2. Optimization Elision (Java 9+): When the terminal operation does not require element values to compute its result (e.g., count() on a stream source of known size), the JVM implementation is permitted to skip intermediate operations entirely, meaning peek will never execute!
// In Java 9+, the JVM knows the exact size from Stream.of() and may optimize count():
long count = Stream.of("a", "b", "c")
    .peek(System.out::println) // Might NOT be printed!
    .count();
Test Your Knowledge

Consider the following code snippet:

List<Integer> numbers = List.of(2, 4, 6, 7, 8, 10);
List<Integer> taken = numbers.stream()
                             .takeWhile(n -> n % 2 == 0)
                             .toList();
List<Integer> dropped = numbers.stream()
                               .dropWhile(n -> n % 2 == 0)
                               .toList();
System.out.println(taken + " " + dropped);

What is the output of this program?

A

[2, 4, 6] [7, 8, 10]

B

[2, 4, 6, 7, 8, 10] []

C

[2, 4, 6] [7]

D

[2, 4, 6, 8, 10] [7]

Test Your Knowledge

Given the following code snippet:

List<List<String>> matrix = List.of(
    List.of("alpha", "beta"),
    List.of("gamma"),
    List.of("delta", "epsilon")
);
long count = matrix.stream()
                   .flatMap(list -> list.stream())
                   .filter(s -> s.contains("e"))
                   .count();
System.out.println(count);

What is printed to standard output when this code is executed?

A

Compilation fails because flatMap requires a function returning a Collection instead of a Stream.

B

2

C

5

D

3

Test Your Knowledge

Consider the following class definition and execution code:

class Container {
    int value;
    Container(int value) { this.value = value; }
}

List<Container> items = List.of(new Container(3), new Container(1), new Container(2));
List<Container> sorted = items.stream()
                              .sorted()
                              .toList();

What is the outcome of compiling and running this code?

A

A ClassCastException is thrown at runtime because Container does not implement Comparable.

B

Compiles cleanly and preserves the original insertion order: 3, 1, 2.

C

Compiles cleanly and sorts the containers by their System identity hash codes.

D

Compilation fails on the .sorted() call because Container does not implement Comparable.

Sections you finish are checked off in the contents.