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.
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 auxiliary memory and parallelize effortlessly across CPU cores.
- Examples:
filter,map,mapToInt,flatMap,peek.
- Examples:
- 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 (
sortedanddistinctcan hold the whole stream, roughly memory) or track position (limit,skip), and they add coordination cost in parallel streams.- Examples:
distinct,sorted,limit,skip,takeWhile,dropWhile.
- Examples:
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 implementComparable<? super T>.sorted(Comparator<? super T>): Sorts elements according to an explicitComparator.
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 mostmaxSizeelements. It is a short-circuiting stateful operation.skip(n): Discards the firstnelements 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 totrue. The moment an element evaluates tofalse,takeWhileimmediately 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 totrue. The moment an element evaluates tofalse,dropWhilestops 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]
| Method | Behavior on Ordered Stream | Short-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:
- Non-interference violation: Modifying shared mutable state inside
peekcauses race conditions in parallel pipelines. - 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, meaningpeekwill 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();
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?
[2, 4, 6] [7, 8, 10]
[2, 4, 6, 7, 8, 10] []
[2, 4, 6] [7]
[2, 4, 6, 8, 10] [7]
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?
Compilation fails because flatMap requires a function returning a Collection instead of a Stream.
2
5
3
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 ClassCastException is thrown at runtime because Container does not implement Comparable.
Compiles cleanly and preserves the original insertion order: 3, 1, 2.
Compiles cleanly and sorts the containers by their System identity hash codes.
Compilation fails on the .sorted() call because Container does not implement Comparable.
Sections you finish are checked off in the contents.