What Are Java Streams?

Introduced in Java 8, the Streams API provides a declarative way to process sequences of elements. Rather than writing verbose loops, you describe what you want to do — filter, transform, collect — and the API handles the how. Streams also make it easy to parallelize operations with a single method call.

Importantly, a Stream is not a data structure. It doesn't store data — it processes data from a source (a collection, array, or I/O channel) through a pipeline of operations.

Creating a Stream

// From a collection
List<String> names = List.of("Alice", "Bob", "Charlie", "Diana");
Stream<String> stream = names.stream();

// From an array
Stream<Integer> nums = Arrays.stream(new Integer[]{1, 2, 3, 4});

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

The Three Parts of a Stream Pipeline

  1. Source — Where data comes from (collection, array, etc.)
  2. Intermediate operations — Transform the stream; they are lazy (don't execute until a terminal operation is called)
  3. Terminal operation — Triggers execution and produces a result or side effect

Core Intermediate Operations

filter()

Keeps only elements matching a predicate:

List<String> longNames = names.stream()
    .filter(name -> name.length() > 4)
    .collect(Collectors.toList());
// ["Alice", "Charlie", "Diana"]

map()

Transforms each element:

List<String> upperNames = names.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toList());
// ["ALICE", "BOB", "CHARLIE", "DIANA"]

sorted()

Sorts elements (natural order or with a comparator):

List<String> sorted = names.stream()
    .sorted()
    .collect(Collectors.toList());

distinct() and limit()

Stream.of(1, 2, 2, 3, 3, 3)
    .distinct()    // removes duplicates
    .limit(4)      // takes at most 4 elements
    .forEach(System.out::println);

Core Terminal Operations

collect()

Accumulates stream elements into a collection:

List<String> result = names.stream()
    .filter(n -> n.startsWith("A"))
    .collect(Collectors.toList());

reduce()

Reduces a stream to a single value:

int sum = IntStream.of(1, 2, 3, 4, 5)
    .reduce(0, Integer::sum);
// 15

count(), findFirst(), anyMatch()

long count = names.stream().filter(n -> n.length() > 3).count();

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

boolean hasAlice = names.stream().anyMatch(n -> n.equals("Alice")); // true

Parallel Streams

You can parallelize stream processing with a single change:

names.parallelStream()
    .filter(name -> name.length() > 4)
    .forEach(System.out::println);

Parallel streams split the workload across available CPU cores. Use them for large datasets and computationally intensive operations. For small collections, the overhead of parallelism can actually be slower than sequential processing.

Streams vs. Loops: When to Use Which

SituationPrefer
Simple iteration with side effectsTraditional loop
Transforming/filtering collectionsStreams
Chaining multiple operationsStreams
Need break/continue logicTraditional loop
Large dataset, CPU-bound processingParallel streams

Key Takeaways

  • Streams make collection processing more readable and concise.
  • Intermediate operations are lazy — they only run when a terminal operation is called.
  • A stream can only be consumed once; create a new one if you need to re-process.
  • Use method references (String::toUpperCase) to make lambdas even more concise.