8.1 Stream Creation and Intermediate Operations

Key Takeaways

  • Streams represent lazy, declarative data pipelines divided into source creation, zero or more intermediate transformations, and a single terminal execution phase.
  • Intermediate operations are classified as stateless (filter, map, flatMap, peek) or stateful (sorted, distinct, limit, skip, takeWhile, dropWhile), with execution deferred until a terminal operation is invoked.
  • Java 9 introduced takeWhile and dropWhile for predicate-based stream boundary slicing, behaving deterministically on ordered streams and stopping evaluation immediately upon condition mismatch.
  • Streams are single-use pipelines; invoking a terminal operation or closing a stream consumes it permanently, causing any subsequent operation on that stream instance to throw an IllegalStateException.
Last updated: September 2026

Stream Creation and Intermediate Operations

The Stream API, introduced in Java 8 and continuously enhanced through Java 9 to Java 21, provides a functional, declarative approach to processing sequences of elements. Unlike collections, streams do not store data; instead, they convey elements from a source (such as a collection, array, I/O channel, or generator function) through a pipeline of computational steps.

For the Oracle Certified Professional: Java SE 21 Developer (1Z0-830) exam, you must master the mechanics of stream construction, lazy evaluation, vertical pipeline processing, stateless versus stateful intermediate transformations, and stream boundary slicing (takeWhile and dropWhile).


1. The Stream Pipeline Architecture & Lifecycle

A stream pipeline consists of three fundamental components:

  1. A Data Source: Supplies elements to the pipeline (e.g., List, Set, Map, array, file I/O, or generator function).
  2. Zero or More Intermediate Operations: Transform the stream into another stream (e.g., filter, map, flatMap, sorted). Intermediate operations are always lazy; no computation occurs until the pipeline is activated.
  3. Exactly One Terminal Operation: Triggers the traversal of the pipeline, processes the elements, produces a non-stream result (e.g., List, long, Optional, boolean) or side-effect, and closes/consumes the stream.
+-------------+      +-------------------------+      +--------------------+      +--------------------+
| Data Source | ---> | Intermediate Op (Lazy) | ---> | Intermediate Op... | ---> | Terminal Operation |
| (List/Array)|      |  filter(Predicate<T>)   |      |  map(Function<T,R>)|      |  collect(toList()) |
+-------------+      +-------------------------+      +--------------------+      +--------------------+
                                                                                           │
                                      Execution Triggered Eagerly <────────────────────────┘

Lazy Evaluation & Vertical Pipelining

Java streams execute vertically (element-by-element through all chained operations) rather than horizontally (evaluating each operation across the entire dataset before moving to the next). This vertical fusion minimizes intermediate memory allocations and allows short-circuiting operations to stop pipeline execution early.

List<String> names = List.of("Alexander", "Brian", "Charles", "David", "Edward");

String result = names.stream()
    .filter(s -> {
        System.out.println("filter: " + s);
        return s.length() > 5;
    })
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase();
    })
    .findFirst()
    .orElse("NONE");

// Console Output demonstrates vertical execution (Brian, Charles, David, Edward are never mapped):
// filter: Alexander
// map: Alexander

[!IMPORTANT] The Single-Use Rule: A stream instance can only be operated upon (traversed via a terminal operation) once. Once a terminal operation has been executed or the stream has been closed, attempting to invoke any intermediate or terminal operation on that same Stream reference throws java.lang.IllegalStateException: stream has already been operated upon or closed.


2. Stream Creation Factories & Data Sources

Java provides multiple mechanisms to construct both finite and infinite streams:

Standard Collection & Array Sources

// 1. From Collections
List<String> list = List.of("Java", "Kotlin", "Scala");
Stream<String> s1 = list.stream();
Stream<String> s2 = list.parallelStream();

// 2. From Arrays
String[] array = {"Alpha", "Beta", "Gamma", "Delta"};
Stream<String> s3 = Arrays.stream(array);
Stream<String> s4 = Arrays.stream(array, 1, 3); // Sub-array [1, 3) -> "Beta", "Gamma"

// 3. From Static Factory Methods
Stream<String> s5 = Stream.of("One", "Two", "Three");
Stream<String> s6 = Stream.ofNullable(null); // Java 9: Produces Stream.empty() instead of NullPointerException
Stream<String> s7 = Stream.empty();

Infinite & Generator Streams

Infinite streams generate elements on demand. They must typically be bounded using short-circuiting intermediate operations like limit() before triggering a terminal operation.

// Stream.generate(Supplier<T>)
Stream<Double> randoms = Stream.generate(Math::random).limit(5);

// Stream.iterate(T seed, UnaryOperator<T> f) - Infinite
Stream<Integer> evens = Stream.iterate(0, n -> n + 2).limit(10);

// Stream.iterate(T seed, Predicate<T> hasNext, UnaryOperator<T> f) - Java 9 Finite (like a for-loop)
Stream<Integer> finiteIterate = Stream.iterate(1, n -> n <= 100, n -> n * 2);
// Yields: 1, 2, 4, 8, 16, 32, 64

I/O and Character Sources

// From Files (AutoCloseable: Always manage with try-with-resources!)
try (Stream<String> lines = Files.lines(Path.of("application.log"))) {
    lines.filter(line -> line.contains("ERROR"))
         .forEach(System.out::println);
}

// From String characters (yields IntStream of UTF-16 code units or code points)
IntStream chars = "Java 21".chars();
IntStream codePoints = "Java 21".codePoints();

// From Regular Expressions
Pattern pattern = Pattern.compile(",\\s*");
Stream<String> tokens = pattern.splitAsStream("apple, orange, banana, grape");

// Concatenating Streams
Stream<String> concatenated = Stream.concat(Stream.of("A", "B"), Stream.of("C", "D"));

// Stream Builder
Stream<String> built = Stream.<String>builder()
    .add("First")
    .add("Second")
    .build();

3. Stateless Intermediate Operations

Stateless intermediate operations process each element independently without retaining state or memory of previously processed elements. They are highly efficient and scale linearly in parallel pipelines.

OperationMethod SignatureDescription
filterStream<T> filter(Predicate<? super T> predicate)Retains elements matching the given boolean predicate.
map<R> Stream<R> map(Function<? super T, ? extends R> mapper)Performs 1-to-1 element transformation.
flatMap<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)Performs 1-to-N transformation and flattens nested streams into a single composite stream.
mapMulti<R> Stream<R> mapMulti(BiConsumer<? super T, ? super Consumer<R>> mapper)Java 16+ low-overhead imperative alternative to flatMap for replacing elements with zero or more items.
peekStream<T> peek(Consumer<? super T> action)Executes an action on each element as it passes through the pipeline; intended for debugging.
unorderedStream<T> unordered()Relaxes ordering constraints on the pipeline to allow performance optimizations in parallel streams.

Deep Dive: map vs. flatMap vs. mapMulti

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

// map: Results in Stream<List<String>> (does not flatten)
Stream<List<String>> mapped = nested.stream().map(l -> l);

// flatMap: Flattens into Stream<String>
List<String> flatResult = nested.stream()
    .flatMap(Collection::stream)
    .map(String::toLowerCase)
    .toList(); // ["a", "b", "c", "d", "e"]

// mapMulti (Java 16+): Avoids creating intermediate Stream instances
List<String> multiResult = List.of("hello world", "stream api").stream()
    .<String>mapMulti((str, consumer) -> {
        for (String word : str.split(" ")) {
            consumer.accept(word.toUpperCase());
        }
    })
    .toList(); // ["HELLO", "WORLD", "STREAM", "API"]

[!WARNING] The peek() Pitfall on 1Z0-830: peek() is designed solely to support debugging. Never write business logic inside peek(). Starting in Java 9, stream implementations may optimize away intermediate operations if the terminal operation does not require element evaluation (such as Stream.of("a", "b").peek(System.out::print).count()), resulting in peek() never executing!


4. Stateful Intermediate Operations

Stateful intermediate operations require knowledge of previously processed elements or must buffer the entire dataset before emitting elements downstream.

OperationMethod SignatureStateful Behavior & Constraints
distinct()Stream<T> distinct()Uses equals() and hashCode() to filter out duplicates; retains state of seen elements.
sorted()Stream<T> sorted()<br>Stream<T> sorted(Comparator<? super T> comp)Buffers all upstream elements before sorting; requires comparable elements or explicit Comparator.
limit(n)Stream<T> limit(long maxSize)Short-circuiting; truncates the stream to at most maxSize elements.
skip(n)Stream<T> skip(long n)Discards the first n elements; emits remaining elements.
takeWhile(p)Stream<T> takeWhile(Predicate<? super T> p)Java 9: Takes elements as long as predicate matches; stops immediately on first mismatch in ordered streams.
dropWhile(p)Stream<T> dropWhile(Predicate<? super T> p)Java 9: Drops elements while predicate matches; once false is encountered, emits all remaining elements.

takeWhile() vs. dropWhile() (Java 9+)

takeWhile and dropWhile operate on stream boundaries based on an element condition. Their behavior is critically dependent on stream order:

List<Integer> numbers = List.of(2, 4, 6, 7, 8, 10, 12);

// takeWhile: Stops at 7 (first element where n % 2 == 0 is false)
List<Integer> taken = numbers.stream()
    .takeWhile(n -> n % 2 == 0)
    .toList(); 
// Output: [2, 4, 6]

// dropWhile: Drops 2, 4, 6; starts emitting at 7 and emits everything thereafter (even 8, 10, 12)
List<Integer> dropped = numbers.stream()
    .dropWhile(n -> n % 2 == 0)
    .toList(); 
// Output: [7, 8, 10, 12]

takeWhile vs. filter Comparison

  • filter(p) evaluates every element in the stream against the predicate (unless bounded by limit).
  • takeWhile(p) halts evaluation at the first failure in an ordered stream, making it safe for ordered infinite streams where a condition eventually fails.
// Safe on infinite stream because takeWhile short-circuits:
List<Integer> powersOfTwo = Stream.iterate(1, n -> n * 2)
    .takeWhile(n -> n < 100)
    .toList(); // [1, 2, 4, 8, 16, 32, 64]

// HANGS / INFINITE LOOP: filter does not know the sequence is monotonic!
// Stream.iterate(1, n -> n * 2).filter(n -> n < 100).toList();

5. Non-Interference Rule and Common Traps

The Non-Interference Rule

Stream operations must be non-interfering. This means that during the execution of a stream pipeline, the stream's data source must not be structurally modified (elements added, removed, or replaced).

List<String> list = new ArrayList<>(List.of("alpha", "beta", "gamma"));

// VIOLATION: Mutating the backing source inside a stream operation
try {
    list.stream()
        .peek(s -> {
            if (s.equals("beta")) {
                list.add("delta"); // Modifying backing collection!
            }
        })
        .forEach(System.out::println);
} catch (ConcurrentModificationException e) {
    System.out.println("Caught ConcurrentModificationException due to interference!");
}

Stateful Operations on Infinite Streams Trap

Applying sorted() or distinct() to an unbounded infinite stream without a preceding limit() or takeWhile() causes an infinite loop or OutOfMemoryError, because the stateful operation must buffer all elements before emitting any.

Loading diagram...
Stream Pipeline Lifecycle, Intermediate Classification, and Slicing
Test Your Knowledge

Examine the following code snippet:

var list = Stream.iterate(1, n -> n <= 20, n -> n + 3)
    .takeWhile(n -> n < 15)
    .dropWhile(n -> n < 7)
    .toList();
System.out.println(list);
What is the output of executing this code?

A
B
C
D
Test Your Knowledge

Given the following code snippet:

List<List<String>> matrix = List.of(
    List.of("alpha", "beta"),
    List.of("gamma", "delta"),
    List.of("epsilon")
);

long count = matrix.stream()
    .flatMap(Collection::stream)
    .filter(s -> s.length() > 4)
    .map(String::toUpperCase)
    .count();

System.out.println(count);
What is the result of running this code?

A
B
C
D
Test Your Knowledge

Consider the following code using a Stream pipeline:

List<Integer> list = List.of(10, 20, 30, 40, 5, 50, 60);
List<Integer> result = list.stream()
    .dropWhile(n -> n >= 20)
    .filter(n -> n % 2 == 0)
    .toList();
System.out.println(result);
What is the printed result?

A
B
C
D
Test Your Knowledge

What happens when the following code is compiled and executed?

Stream<String> stream = Stream.of("red", "green", "blue");
long c1 = stream.filter(s -> s.startsWith("g")).count();
long c2 = stream.filter(s -> s.startsWith("b")).count();
System.out.println(c1 + " " + c2);
What is the result?

A
B
C
D