Skip to content

Stream API — Creation, Intermediate Ops, Terminal Ops, and Parallel Streams

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Stream API. We cover key concepts, practical examples, and best practices to help you master this topic.

The Java Stream API processes sequences of elements with functional-style operations, enabling declarative data processing pipelines. Streams let you Express complex data transformations — filtering, mapping, reducing — as a chain of operations on a source, without explicit loops or mutable state.

What You'll Learn

  • Creating streams from collections, arrays, and generators
  • Intermediate operations: filter, map, flatMap, distinct, sorted, peek
  • Terminal operations: collect, reduce, count, anyMatch, forEach
  • Lazy evaluation and stream characteristics
  • Parallel streams for multi-threaded processing

Why It Matters

Streams lead to more readable, less error-prone code. A stream pipeline reads like a problem description: "filter inactive users, map to names, sort, collect to list" — no nested loops, no temporary variables, no off-by-one errors.

Real-World Use

Batch processing (transform millions of records), Etl Pipelines, report generation, and data validation all use streams. Spring Data JPA returns streams from database queries.


Creating Streams

// From collections
List<String> list = List.of("a", "b", "c");
Stream<String> stream = list.stream();
Stream<String> parallelStream = list.parallelStream();

// From arrays
int[] numbers = {1, 2, 3};
IntStream intStream = Arrays.stream(numbers);
Stream<String> stringStream = Stream.of("a", "b", "c");

// From values
Stream<Integer> values = Stream.of(1, 2, 3);
Stream<Object> empty = Stream.empty();

// Infinite streams
Stream<Integer> naturals = Stream.iterate(0, n -> n + 1);
Stream<Double> randoms = Stream.generate(Math::random);

Primitive Streams

IntStream.range(1, 10);      // 1..9
IntStream.rangeClosed(1, 10); // 1..10
LongStream.range(0, 100);
DoubleStream.generate(Math::random);

Intermediate Operations

Intermediate operations return a new stream. They are lazy — nothing happens until a terminal operation is invoked.

filter

List<String> names = List.of("Alice", "Bob", "Charlie", "David");
names.stream()
    .filter(name -> name.length() > 4)
    .forEach(System.out::println);
// Alice
// Charlie

map

names.stream()
    .map(String::toUpperCase)
    .forEach(System.out::println);
// ALICE, BOB, CHARLIE, DAVID

flatMap

Flattens nested structures:

List<List<Integer>> nested = List.of(
    List.of(1, 2),
    List.of(3, 4, 5),
    List.of(6)
);

List<Integer> flat = nested.stream()
    .flatMap(Collection::stream)
    .toList();
// [1, 2, 3, 4, 5, 6]

distinct

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

sorted

names.stream()
    .sorted(Comparator.comparingInt(String::length))
    .forEach(System.out::println);
// Bob, Alice, David, Charlie

peek (Debugging)

long count = names.stream()
    .peek(System.out::println)
    .count();

limit and skip

IntStream.range(0, 100)
    .skip(10)
    .limit(5)
    .forEach(System.out::print); // 10 11 12 13 14

Terminal Operations

Terminal operations produce a result or side effect. After a terminal operation, the stream is consumed.

collect

List<String> result = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet());
String joined = stream.collect(Collectors.joining(", "));

toList() (Java 16+)

List<String> result = stream.toList(); // immutable list

reduce

int sum = IntStream.range(1, 6)
    .reduce(0, (a, b) -> a + b);
// 15

Optional<Integer> sumOpt = stream.reduce(Integer::sum);

count

long count = stream.count();

anyMatch, allMatch, noneMatch

boolean hasLong = names.stream().anyMatch(name -> name.length() > 5);
boolean allShort = names.stream().allMatch(name -> name.length() < 10);
boolean noEmpty = names.stream().noneMatch(String::isEmpty);

findFirst, findAny

Optional<String> first = names.stream()
    .filter(n -> n.startsWith("A"))
    .findFirst();

forEach

stream.forEach(System.out::println);

Pipeline Example

List<Transaction> transactions = getTransactions();

List<String> highValueCustomerNames = transactions.stream()
    .filter(t -> t.getAmount() > 1000)
    .filter(t -> t.getType() == TransactionType.CREDIT)
    .map(Transaction::getCustomerName)
    .distinct()
    .sorted()
    .toList();

Lazy Evaluation

Intermediate operations are not executed until a terminal operation is added:

Stream<String> stream = names.stream()
    .filter(name -> {
        System.out.println("Filtering: " + name);
        return name.length() > 3;
    })
    .map(name -> {
        System.out.println("Mapping: " + name);
        return name.toUpperCase();
    });

// Nothing printed yet — lazy

stream.forEach(System.out::println);
// Filtering: Alice
// Mapping: Alice
// ALICE
// Filtering: Bob
// Filtering: Charlie
// Mapping: Charlie
// CHARLIE

Each element passes through the pipeline vertically (filter -> map -> forEach) rather than horizontally (all filters, then all maps).

Parallel Streams

long sum = LongStream.rangeClosed(0, 10_000_000)
    .parallel()
    .sum();

When to Use

  • Large datasets (thousands of elements)
  • CPU-intensive operations
  • Independent elements (no shared mutable state)

When to Avoid

  • Small datasets (parallel overhead outweighs benefits)
  • I/O-bound operations (blocking threads)
  • Non-thread-safe shared state (race conditions)
  • Ordered operations that require encounter order

Common Mistakes

  1. Reusing a stream after terminal operation. Streams are consumed after one terminal operation. Calling a second terminal operation throws IllegalStateException.
  2. Modifying the source while streaming. If the backing collection is modified during streaming, ConcurrentModificationException may be thrown.
  3. Using parallel() without considering Thread Safety. Shared mutable state requires synchronization.
  4. Assuming findFirst() is faster than findAny() with parallel streams. findAny() is more parallel-friendly.
  5. Using forEach() when collect() is more appropriate. forEach() is for side effects; collect() is for reducing to a result.

Practice Questions

1. What is the difference between intermediate and terminal operations?
Intermediate operations are lazy and return a new stream. Terminal operations produce a result or side effect and consume the stream.

2. How does flatMap differ from map?
map transforms each element to another object (1-to-1). flatMap transforms each element to a stream and flattens the result (1-to-many).

3. What does Stream.of(1, 2, 3).toList() return?
An immutable List<Integer> containing [1, 2, 3] (Java 16+).

4. When should you use parallel streams?
For large datasets with CPU-intensive, independent operations. Avoid for small datasets, I/O operations, or when order matters.

5. Can a stream be reused?
No. A stream can have only one terminal operation. After that, the stream is consumed.

Challenge Question:
Write a method Map<String, List<String>> groupByFirstLetter(List<String> words) that groups words by their first letter. Then use streams to find the most common first letter. Also write a method OptionalDouble median(int[] numbers) that finds the median using streams.

FAQ

What is the difference between `Collection.stream()` and `Stream.of()`?

collection.stream() creates a stream from an existing collection. Stream.of(elements) creates a stream from individual elements or an array.

What is a short-circuiting operation?

Terminal operations like findFirst(), findAny(), anyMatch(), allMatch(), and limit() may process only a subset of elements before returning. They can terminate early.

Are streams faster than loops?

Not necessarily. Streams add abstraction overhead. For simple operations on small lists, a for-each loop is faster. For complex pipelines on large datasets, streams can be faster (especially parallel streams). Readability is the primary advantage.

What is the `Spliterator`?

A Spliterator is the internal iterator used by streams. It supports splitting for parallel processing. Collections provide Spliterators to the stream framework.

Can I convert a stream back to an array?

Yes: stream.toArray(String[]::new) returns String[]. Without a constructor reference, stream.toArray() returns Object[].

Mini Project

Write a program StreamDemo.java that:

  1. Generates a list of 100 random Person objects (name, age, city)
  2. Filters: people older than 18
  3. Maps: extract names and ages
  4. Groups by city using Collectors.groupingBy()
  5. Finds the average age per city using averagingInt()
  6. Finds the top 3 oldest people using sorted() and limit()
  7. Processes the same data with parallel streams and measures time
  8. Uses flatMap to extract all unique letters from all names
  9. Collects results into various forms: List, Set, Map, String

What's Next

Streams and collectors work together. Lesson 35 explores stream collectors in depth — toList, groupingBy, partitioningBy, mapping, teeing, and custom collectors for complex aggregations.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro