Back to Stream API Fundamentals
Q1Question 1 of 8

What is the Stream API?

Key Concepts
  • A stream pipeline has three parts: source (Collection, array, file, generator), intermediate operations (lazy), and terminal operation (triggers execution).
  • Intermediate operations like filter, map, sorted, and distinct return a new Stream and are lazy: they do nothing until a terminal runs.
  • Terminal operations like collect, forEach, reduce, count, and findFirst trigger execution and consume the stream.
  • Streams do not store elements: they process elements on demand from the source in a pull-based model.
  • Streams are single-use: after a terminal operation, the stream is consumed and cannot be reused.
  • Collections store data (eager, reusable, mutable); Streams process data (lazy, single-use, immutable pipeline).
  • Streams support built-in parallelism via parallelStream() while collection parallelism requires manual thread management.

Definition

The Stream API is a pipeline-based abstraction with three parts: source, lazy intermediate operations, and a terminal operation that triggers execution and produces a result.
  • Source provides elements: Collection, array, file, generator, or range.
  • Intermediate operations are lazy transformations that return a new Stream: filter, map, sorted, distinct, flatMap, limit, skip, peek.
  • Terminal operations trigger execution and produce a result: collect, forEach, reduce, count, findFirst, anyMatch.
  • Lazy evaluation means intermediate operations do nothing until a terminal triggers them.
  • Single-use means after a terminal, the stream is consumed and throws IllegalStateException on reuse.
  • No storage means the stream does not hold elements in memory; it processes them on demand from the source.

A Collection is like the bank vault at National Bank of India: it stores cash physically. You go in, count it, sort it, take some out. A Stream is like the cash-counting machine on the assembly line: cash flows through it, gets counted, sorted, and filtered, but the machine itself stores nothing. The cash comes from the vault (source), passes through machines (intermediate operations), and ends at the bag (terminal operation). The machines sit idle until the manager presses start (terminal).

Understanding the Concepts

What is the Stream API?

The Stream API (java.util.stream) was introduced in Java 8 for declarative data processing. Instead of writing imperative for-loops with explicit accumulators, you describe what to compute and the framework handles execution.

What are the three components?

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.List;

public class StreamComponentsDemo {
    record Transaction(String accountId, BigDecimal amount, boolean settled) {}

    public static void main(String[] args) {
        List<Transaction> transactions = List.of(
            new Transaction("SAV2001", new BigDecimal("45000"), true),
            new Transaction("SAV2002", new BigDecimal("12000"), false),
            new Transaction("SAV2001", new BigDecimal("78000"), true),
            new Transaction("CUR3001", new BigDecimal("250000"), true)
        );

        BigDecimal total = transactions.stream()              // SOURCE
            .filter(Transaction::settled)                      // INTERMEDIATE (lazy)
            .map(Transaction::amount)                          // INTERMEDIATE (lazy)
            .reduce(BigDecimal.ZERO, BigDecimal::add);         // TERMINAL (triggers all)

        System.out.println("Settled total: Rs." + total);
    }
}
TEXT
Settled total: Rs.373000

How is a Stream different from a Collection?

| Aspect | Collection | Stream |
|--------|-----------|--------|
| Storage | Stores elements in memory | Stores nothing: processes on the fly |
| Evaluation | Eager: all elements exist upfront | Lazy: computed only when terminal runs |
| Reuse | Reusable: iterate many times | Single-use: consumed after terminal |
| Mutability | Can add, remove, modify | Cannot modify source |
| How you work | Imperative: for-loop, if, accumulator | Declarative: filter, map, collect |
| Parallelism | Manual thread management | Built-in: parallelStream() |

What are the stream sources?

JAVA
package com.bank.core;

import java.util.*;
import java.util.stream.*;
import java.math.BigDecimal;

public class StreamSources {
    public static void main(String[] args) {
        // From Collection:
        List.of("SAV2001", "SAV2002").stream();

        // From array:
        Stream.of("RELIANCE", "TCS", "HDFC");
        Arrays.stream(new int[]{100, 200, 300});

        // From generator (infinite!):
        Stream.generate(() -> BigDecimal.valueOf(Math.random()));
        Stream.iterate(BigDecimal.ONE, amt -> amt.multiply(BigDecimal.TWO));

        // Bounded iterate (Java 9):
        Stream.iterate(1, n -> n <= 100, n -> n + 1);

        // Empty:
        Stream.empty();
    }
}

What are the intermediate operations?

All intermediate operations are lazy and return a new Stream.

  • filter(Predicate) : keep matching elements
  • map(Function) : transform each element
  • flatMap(Function) : transform and flatten (one-to-many)
  • sorted() / sorted(Comparator) : sort (stateful, buffers all)
  • distinct() : remove duplicates (stateful, uses HashSet)
  • limit(n) : take first n (short-circuit)
  • skip(n) : drop first n
  • peek(Consumer) : debug observation (do not use for business logic)

What are the terminal operations?

Terminal operations trigger execution and consume the stream.

  • collect(Collector) : accumulate into collection, string, or map
  • toList() : collect to unmodifiable list (Java 16)
  • forEach(Consumer) : execute action on each
  • reduce(identity, BinaryOperator) : fold into single value
  • count() : count elements
  • findFirst() / findAny() : return Optional (short-circuit)
  • anyMatch / allMatch / noneMatch : boolean (short-circuit)
  • min(Comparator) / max(Comparator) : return Optional

What happens if you reuse a stream?

JAVA
package com.bank.core;

import java.util.stream.Stream;

public class StreamReuseDemo {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("SAV2001", "SAV2002", "CUR3001");
        long count = stream.count(); // OK: terminal #1
        System.out.println("Count: " + count);

        try {
            stream.toList(); // THROWS IllegalStateException!
        } catch (IllegalStateException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
}
TEXT
Count: 3
Error: stream has already been operated upon or closed

What are common mistakes?

JAVA
package com.bank.core;

import java.util.List;

public class StreamMistakes {
    public static void main(String[] args) {
        List<String> accounts = List.of("SAV2001", "SAV2002");

        // WRONG: no terminal operation, pipeline does nothing
        accounts.stream()
            .filter(a -> a.startsWith("SAV"))
            .peek(System.out::println); // Nothing printed! No terminal!

        // CORRECT: add a terminal operation
        accounts.stream()
            .filter(a -> a.startsWith("SAV"))
            .forEach(System.out::println); // Prints both accounts
    }
}
TEXT
SAV2001
SAV2002

What should a senior engineer know?

Streams are designed to complement Collections, not replace them. Collections store data (the data layer). Streams process data (the computation layer). The typical pattern is: collection.stream() followed by pipeline operations followed by collect() back to a Collection. Streams are particularly powerful for read-only analytical queries, aggregation, grouping, and parallel processing of large datasets.

Visual Overview

DIAGRAM
  Stream Pipeline:
  ─────────────────────────────────────────────
  transactions.stream()              ← SOURCE
       │
       ▼
  .filter(Transaction::settled)      ← INTERMEDIATE (lazy)
       │
       ▼
  .map(Transaction::amount)          ← INTERMEDIATE (lazy)
       │
       ▼
  .reduce(ZERO, BigDecimal::add)     ← TERMINAL (triggers all)
       │
       ▼
  Rs.373000                          ← RESULT

  Without terminal: NOTHING executes.
  After terminal: stream is CONSUMED.
DIAGRAM
  Collection vs Stream:
  ┌───────────────────┐     ┌───────────────────┐
  │ Collection (Vault) │     │ Stream (Machine)   │
  │ Stores elements    │     │ Processes elements │
  │ Eager              │     │ Lazy               │
  │ Reusable           │     │ Single-use         │
  │ Mutable            │     │ Immutable pipeline │
  │ size(), get()      │     │ filter(), map()    │
  └───────────────────┘     └───────────────────┘

Key Takeaway

The Stream API is a pipeline-based abstraction with three components: source, intermediate operations, and terminal operation. The source provides elements from a Collection, array, file, or generator. Intermediate operations like filter, map, sorted, and distinct are lazy: they build a pipeline description but execute nothing until a terminal operation triggers them. Terminal operations like collect, reduce, forEach, and count trigger the entire pipeline and produce a result.

Streams differ from Collections in five fundamental ways. Streams do not store elements. Streams are lazy while collections are eager. Streams are single-use while collections are reusable. Streams are declarative while collection processing is imperative. Streams support built-in parallelism via parallelStream().

In Rajesh's settlement report service at National Bank of India, the stream pipeline filters settled transactions, maps to amounts, and reduces to a total. Without the terminal reduce, nothing executes. After the terminal, the stream is consumed and cannot be reused.

Remember one sentence: a stream pipeline is source to lazy intermediates to terminal, and nothing runs until the terminal triggers it.

---

Share
Use ← → arrow keys to navigate