Study guide

8.1 Stream Lifecycle: Creation, Pipeline Architecture, and Lazy Evaluation

Oracle Java SE 17 Developer Certified Professional 1Z0-829

Track your progress as you read

Finished sections get a check mark.

1 Introduction: The 1Z0-829 Exam

About the Java SE 17 Developer (1Z0-829) Exam

2 Chapter 1: Java Fundamentals & Language Basics

1.1 Primitive Types, Literals, Wrapper Classes, and Type Promotion/Casting 1.2 Local Variable Type Inference (`var`) Rules and Scoping 1.3 Operators, Precedence, Evaluation Order, and Short-Circuit Evaluation 1.4 Text Blocks, String Immutability, and StringBuilder APIs

3 Chapter 2: Program Flow & Decision Constructs

2.1 Conditional Branching with if/else and the Ternary Operator 2.2 Switch Statements and Switch Expressions 2.3 Iteration Constructs, Loop Control, and Branching Labels

4 Chapter 3: Object-Oriented Design & Class Mechanics

3.1 Class Declarations, Field/Method Signatures, and Access Modifiers 3.2 Constructors, Overloading, Initialization Order, and Garbage Collection 3.3 Inheritance, Method Overriding, Polymorphism, and `super`/`this` 3.4 Abstract Classes, Static Members, and Object Methods (`equals`, `hashCode`, `toString`)

5 Chapter 4: Advanced OOP: Interfaces, Records & Sealed Types

4.1 Interface Architecture: Default, Static, and Private Methods 4.2 Enumerations: Constants, Constructors, Methods, and Custom Bodies 4.3 Records: Canonical/Compact Constructors, Components, and Immutability 4.4 Sealed Classes and Interfaces: `permits`, Hierarchy Constraints, and Modifiers

6 Chapter 5: Pattern Matching & Nested Types

5.1 Pattern Matching for `instanceof` and Flow Scoping Mechanics 5.2 Nested Types: Member Inner, Static Nested, Local, and Anonymous Classes 5.3 Defensive Programming, Immutability Patterns, and Validation

7 Chapter 6: Generics, Collections & Arrays

6.1 Generic Classes, Interfaces, Methods, and Type Erasure 6.2 Bounded Type Parameters and Wildcards (PECS Rule) 6.3 Collections Framework: List, Set, Queue, and Deque APIs 6.4 The Map Interface, Sorting, Comparable, and Comparator 6.5 Arrays: Declaration, Initialization, Multidimensional Arrays, and the Arrays Utility Class

8 Chapter 7: Functional Programming & Lambda Expressions

7.1 Functional Interfaces and Lambda Syntax Rules 7.2 Standard Functional Interfaces in `java.util.function` 7.3 Method References: Static, Bound, Unbound, and Constructor References

9 Chapter 8: The Stream API

8.1 Stream Lifecycle: Creation, Pipeline Architecture, and Lazy Evaluation 8.2 Intermediate Stream Operations: Filtering, Mapping, Sorting, and Slicing 8.3 Terminal Stream Operations: Reduction, Matching, Finding, and Collecting 8.4 Primitive Streams (`IntStream`, `LongStream`, `DoubleStream`) and Numeric Statistics

10 Chapter 9: Advanced Streams & Collectors

9.1 The Optional Class: Safe Null Handling, Transformation, and Chaining 9.2 The Collectors API: Downstream Reductions, Grouping, and Partitioning 9.3 Parallel Streams: Fork/Join Pool, Spliterators, and Concurrency Pitfalls

11 Chapter 10: Exception Handling

10.1 The Java Exception Hierarchy: Checked vs. Unchecked Exceptions and Errors 10.2 Exception Constructs: `try-catch-finally`, Multi-Catch, and Rethrowing 10.3 Try-With-Resources, `AutoCloseable`, Suppressed Exceptions, and Custom Exceptions

12 Chapter 11: Date/Time API & Localization

11.1 Date and Time Core Types: `LocalDate`, `LocalTime`, `LocalDateTime` 11.2 Timestamps, Intervals, and Durations: `Instant`, `Period`, and `Duration` 11.3 Time Zones, Daylight Saving Time (DST) Offsets, and `ZonedDateTime` 11.4 Internationalization and Localization: `Locale`, `ResourceBundle`, and Formatters 11.5 Formatting and Parsing Messages, Numbers, Percentages, and Localized Dates

13 Chapter 12: Java Platform Module System (JPMS)

12.1 Module Architecture, `module-info.java`, and Module Paths 12.2 Module Directives: `requires`, `requires transitive`, `exports`, and Qualified Exports 12.3 Reflection, Deep Encapsulation, `opens`, and Open Modules 12.4 Services in JPMS (`uses`, `provides...with`), Migration Paths, and Tooling (`jlink`, `jdeps`) 12.5 Compiling and Packaging Code: Non-Modular JARs, Modular JARs, and Migration Strategies

14 Chapter 13: Concurrency & Multithreading

13.1 Thread Fundamentals, `Runnable`, `Callable`, and Thread Lifecycle 13.2 Memory Visibility, `volatile`, Intrinsic Locks, and Synchronization 13.3 The Concurrency Framework: `ExecutorService`, Thread Pools, and `Future` 13.4 Concurrent Collections, Atomic Variables, Explicit Locks, and `CompletableFuture`

15 Chapter 14: Java I/O and NIO.2

14.1 Classical I/O: Byte and Character Streams, Readers, Writers, and Buffering 14.2 Object Serialization, `Serializable`, `transient`, and `serialVersionUID` 14.3 NIO.2 Architecture: `Path`, `Paths`, and Basic `Files` Operations 14.4 Advanced NIO.2: Directory Walking, Streams (`Files.lines`, `walk`), and `WatchService`

16 Chapter 15: Accessing Databases with JDBC

15.1 JDBC Architecture, Drivers, `DriverManager`, and `DataSource` Connections 15.2 SQL Execution: `Statement`, `PreparedStatement`, and `CallableStatement` 15.3 `ResultSet` Navigation, Data Types, and Transaction Management (Commit, Rollback, Savepoints)

17 Chapter 16: Annotations & the Java Logging API

16.1 Standard Annotations: @Override, @FunctionalInterface, @Deprecated, @SuppressWarnings, and @SafeVarargs 16.2 Java Logging API Basics: Logger, Levels, Handlers, and Configuration

8.1 Stream Lifecycle: Creation, Pipeline Architecture, and Lazy Evaluation

Key Takeaways Explain This Topic AI

A stream pipeline consists of exactly three sequential stages: a data source, zero or more intermediate operations, and a single terminal operation that triggers processing and consumes the stream.

Streams do not store elements and do not modify their underlying data sources; they are purely computational pipelines that carry values from a source through declarative transformations.

The single-use rule dictates that a stream can be traversed exactly once; invoking any intermediate or terminal operation on a consumed or closed stream throws an IllegalStateException.

Stream creation methods include Collection.stream(), Arrays.stream(), Stream.of(), Stream.empty(), Stream.concat(), Files.lines(), Random.ints(), and infinite generators Stream.generate() and Stream.iterate().

Intermediate operations are strictly lazily evaluated, executing on-demand on an element-by-element (vertical) basis only when a terminal operation triggers pipeline execution.

Last updated: October 2026

8.1 Stream Lifecycle: Creation, Pipeline Architecture, and Lazy Evaluation

Introduced in Java 8 and continuously refined through Java SE 17, the Stream API ( java.util.stream ) provides a declarative, functional approach to processing sequences of elements. Unlike traditional collections, which are in-memory data structures focused on element storage and indexed access, a stream is an abstraction for expressing computations. Streams do not store elements, do not modify the underlying data source, and are designed for high-performance sequential and parallel data transformations.

The Stream Pipeline Architecture

A Java stream pipeline models a data processing query composed of three distinct architectural components:

Data Source : Provides the sequence of elements to the pipeline. Sources include collections, arrays, generator functions, I/O channels, or numeric ranges.

Intermediate Operations : Zero or more declarative transformations (such as filter , map , sorted , and distinct ) that transform the stream into another stream. Intermediate operations are always lazy ; they construct a query plan without executing any computation until prompted.

Terminal Operation : Exactly one terminal operation (such as forEach , count , collect , reduce , or findFirst ) that triggers element evaluation, executes the pipeline, produces a non-stream result or side effect, and marks the stream as consumed. +-------------+ +-------------------------+ +--------------------+ +--------------------+

  • | Data Source | ----> | Intermediate Operation | ----> | Intermediate Oper. | ----> | Terminal Operation |
  • | (List/Array)| | (filter - lazy) | | (map - lazy) | | (collect - eager) |
  • +-------------+ +-------------------------+ +--------------------+ +--------------------+
  • |
  • v
  • Result / End
  • Non-Interference and Immutability

Stream pipelines adhere to the principle of non-interference : intermediate and terminal operations must not modify the backing data source during stream execution. In addition, functional arguments passed to stream operations should be stateless and side-effect-free to prevent unpredictable bugs and race conditions, particularly when pipelines are parallelized. List<String> titles = new ArrayList<>(List.of("Java", "SE", "17"));

  • // ANTI-PATTERN: Structural interference during pipeline execution
  • // titles.stream().peek(item -> titles.remove(item)).forEach(System.out::println);

// Throws ConcurrentModificationException at runtime!

The Single-Use Rule and IllegalStateException

A fundamental property of the Stream API is the single-use rule : a stream instance can be operated upon or traversed exactly once . Once a terminal operation is invoked or the stream's .close() method is called, the stream is considered consumed and exhausted.

Attempting to invoke any intermediate or terminal operation on a previously consumed stream results in an immediate runtime exception: List<String> languages = List.of("Java", "Kotlin", "Scala", "Groovy");

Stream<String> stream = languages.stream().filter(s -> s.startsWith("J"));

  • long count = stream.count(); // Terminal operation 1: consumes the stream
  • System.out.println("Count: " + count); // Prints 1

// Attempting to reuse the same stream instance:

  • // List<String> list = stream.toList();
  • // RUNTIME EXCEPTION: java.lang.IllegalStateException: stream has already been operated upon or closed

Warning

The Stream Reuse Trap An IllegalStateException is thrown even if the preceding terminal operation was short-circuited or returned an empty result. If an application requires multiple operations over the same data source, you must obtain a new stream instance for each traversal. A common professional pattern is using a Supplier<Stream<T>> : Supplier<Stream<String>> streamSupplier = () -> languages.stream().filter(s -> s.length() > 4);

  • long count = streamSupplier.get().count(); // Creates fresh stream 1
  • List<String> list = streamSupplier.get().toList(); // Creates fresh stream 2
  • Stream Creation Sources

Java provides numerous factory methods and API hooks to create streams from diverse data structures and generators.

1. Collections and Maps

The java.util.Collection interface defines default methods for stream generation:

stream() : Returns a sequential Stream<E> with the collection as its source.

parallelStream() : Returns a parallel Stream<E> capable of concurrent execution via the common ForkJoinPool .

Because Map does not implement Collection , maps cannot be streamed directly. Instead, you stream map views: Map<Integer, String> codes = Map.of(200, "OK", 404, "Not Found");

  • Stream<Integer> keyStream = codes.keySet().stream();
  • Stream<String> valueStream = codes.values().stream();
  • Stream<Map.Entry<Integer, String>> entryStream = codes.entrySet().stream();
  • 2. Arrays and Stream.of()

Streams can be created from arrays via Arrays.stream() or Stream.of() : String[] names = { "Alice", "Bob", "Charlie" };

  • Stream<String> s1 = Arrays.stream(names);
  • Stream<String> s2 = Stream.of(names);
  • Stream<String> s3 = Stream.of("Alpha", "Beta", "Gamma"); // Varargs
  • Stream<String> s4 = Arrays.stream(names, 0, 2); // Sliced: index 0 (inclusive) to 2 (exclusive)

Warning

The Primitive Array Stream.of() Trap The static method Stream.of(T... values) accepts reference types. When passed a primitive array (e.g., int[] numbers = {1, 2, 3}; ), Java cannot bind int to type parameter T . Instead, it binds the entire array object int[] as a single element of type T . As a result, Stream.of(numbers) produces a Stream<int[]> of size 1 rather than a stream of numbers!

To obtain a primitive numeric stream, always use Arrays.stream(int[]) or IntStream.of(int...) : int[] nums = { 10, 20, 30 };

  • Stream<int[]> objStream = Stream.of(nums); // Size: 1 (Stream<int[]>)
  • IntStream primStream = Arrays.stream(nums); // Size: 3 (IntStream)
  • 3. Empty and Nullable Streams

Stream.empty() : Creates an empty sequential stream with zero elements. Useful as a defensive default return value.

Stream.ofNullable(T t) : Introduced in Java 9. If the argument is non-null, returns a single-element stream ( Stream.of(t) ); if null , returns Stream.empty() . String value = getNullableConfiguration();

  • Stream<String> configStream = Stream.ofNullable(value); // Safe: no null checks required
  • 4. Infinite Streams: generate() and iterate()

Infinite streams generate an unbounded sequence of elements on demand and must typically be bounded using the limit() intermediate operation:

Stream.generate(Supplier<T> s) : Creates an infinite, unordered stream where each element is produced by invoking the given supplier. Stream<Double> randoms = Stream.generate(Math::random).limit(5);

Stream<UUID> ids = Stream.generate(UUID::randomUUID).limit(10);

Stream.iterate(T seed, UnaryOperator<T> f) : Creates an infinite, ordered sequential stream produced by iterative application of function f to the seed: s e e d , f ( s e e d ) , f ( f ( s e e d ) ) , … seed, f(seed), f(f(seed)), \dots see d , f ( see d ) , f ( f ( see d )) , … Stream<Integer> powersOfTwo = Stream.iterate(1, n -> n * 2).limit(8);

// Yields: 1, 2, 4, 8, 16, 32, 64, 128

Stream.iterate(T seed, Predicate<? super T> hasNext, UnaryOperator<T> next) : Introduced in Java 9, this 3-argument overload provides a finite stream that mimics a traditional for loop: // Equivalent to: for (int i = 1; i <= 16; i *= 2)

  • Stream<Integer> finiteStream = Stream.iterate(1, n -> n <= 16, n -> n * 2);
  • // Yields: 1, 2, 4, 8, 16 (automatically terminates without limit)
  • 5. Stream Concatenation and I/O Streams

Stream.concat(Stream<? extends T> a, Stream<? extends T> b) : Lazily concatenates two streams. Closing the concatenated stream invokes the close handlers of both input streams.

Files.lines(Path path) : Returns a lazy Stream<String> reading text lines from a file. Critical rule : Files.lines() opens an underlying file channel. Because streams do not automatically close upon consumption, Files.lines() must be enclosed within a try-with-resources statement to prevent operating system file handle leaks. Path logFile = Path.of("application.log");

  • try (Stream<String> lines = Files.lines(logFile)) {
  • long errorCount = lines.filter(line -> line.contains("ERROR")).count();
  • System.out.println("Errors: " + errorCount);
  • }
  • Lazy Evaluation Mechanics

Intermediate operations are strictly lazy . When you invoke intermediate operations on a stream, no data is pulled from the source, and no transformations are performed. The stream merely records the operation inside an internal linked pipeline of pipeline stages ( ReferencePipeline ).

Execution begins only when a terminal operation is invoked.

Vertical Execution vs. Horizontal Processing

In traditional procedural code or SQL database engines, data transformations often process horizontally (all elements filtered, stored in a temporary buffer, then all mapped). In Java streams, execution proceeds vertically : an individual element travels through the entire chain of intermediate operations down to the terminal operation before the next element is pulled from the source. List<String> data = List.of("apple", "banana", "apricot", "blueberry");

  • List<String> result = data.stream()
  • .filter(s -> {
  • System.out.println("filter: " + s);
  • return s.startsWith("a");
  • })
  • .map(s -> {
  • System.out.println("map: " + s);
  • return s.toUpperCase();
  • })
  • .limit(1) // Short-circuits after 1 element satisfies the pipeline
  • .toList();
  • Output of vertical execution: filter: apple
  • map: apple

Notice that "banana", "apricot", and "blueberry" are never examined by filter or map ! Because limit(1) satisfied the terminal collection after the very first element, the pipeline short-circuited immediately.

Summary of Stream Creation Factories

Factory Method Source Type Boundedness Key Notes

Collection.stream() Any Collection<E> Finite Reflects collection state at pipeline execution time.

Arrays.stream(T[]) Reference or primitive array Finite Overloaded for int[] , long[] , and double[] .

Stream.of(T...) Explicit values or reference array Finite Wraps primitive arrays into single-element Stream<T[]> .

Stream.generate(Supplier) Functional supplier Infinite Unordered; requires limit() to terminate.

Stream.iterate(seed, f) Seed + unary operator Infinite Ordered; infinite without limit() .

Stream.iterate(seed, pred, f) Seed + predicate + operator Finite Java 9+ finite loop replacement.

Stream.concat(s1, s2) Two existing streams Finite/Infinite Closes both input streams when concatenated stream closes.

Files.lines(Path) NIO.2 file channel Finite Implements AutoCloseable ; must use try-with-resources.