Skip to main content

The Stream API

The Stream API (located in the java.util.stream package) is arguably the most famous and widely used feature introduced in Java 8.

It provides a declarative, functional approach to processing collections of objects. Instead of writing verbose for loops and if statements to filter, sort, and transform data, you simply declare what you want to achieve using a pipeline of operations.


What is a Stream?

A Stream is not a data structure. It does not store elements.

Instead, a Stream takes data from a source (like a List or a Set) and carries it through a pipeline of computational operations. Streams do not modify the original data source; they generate a new result based on the operations applied.


The Stream Pipeline

A Stream pipeline consists of three parts:

  1. The Source: Where the data comes from (e.g., list.stream()).
  2. Intermediate Operations: Operations that transform the stream and return a new Stream (e.g., filter(), map()).
  3. A Terminal Operation: The final operation that triggers the processing and produces a result (e.g., collect(), forEach()).

[!IMPORTANT] Streams are Lazy! Intermediate operations are never executed until a Terminal operation is called. If you write a pipeline with 10 intermediate operations but forget the terminal operation at the end, absolutely nothing will happen!


1. Intermediate Operations

These operations return a new Stream, allowing you to chain multiple operations together. Because they expect Functional Interfaces as arguments, we pass Lambdas into them!

  • filter(Predicate<T>): Keeps elements that match the condition.
  • map(Function<T, R>): Transforms each element into something else.
  • sorted(): Sorts the elements.
  • distinct(): Removes duplicate elements.
  • limit(n): Truncates the stream to only contain the first n elements.

2. Terminal Operations

These operations close the stream and return a final result (or void). Once a terminal operation is executed, the stream is considered "consumed" and cannot be used again.

  • collect(Collector): Gathers the final stream elements into a new Collection (like a List or Set).
  • forEach(Consumer<T>): Performs an action on every element (usually printing).
  • count(): Returns the number of elements in the stream.
  • reduce(BinaryOperator<T>): Combines elements to produce a single result (like a sum).

Example: The Power of Streams

Let's look at a real-world example. We have a list of names. We want to:

  1. Find all names that start with the letter "A".
  2. Convert them to UPPERCASE.
  3. Sort them alphabetically.
  4. Save the results into a brand new List.

The Pre-Java 8 Approach (Imperative)

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class LabStream1 {

public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Amanda", "Charlie", "Adam");

// 1. Create a new list for the results
List<String> filteredNames = new ArrayList<>();

// 2. Loop and Filter
for (String name : names) {
if (name.startsWith("A")) {
// 3. Map (Uppercase) and add to list
filteredNames.add(name.toUpperCase());
}
}

// 4. Sort
Collections.sort(filteredNames);

System.out.println(filteredNames);
}
}

The Java 8 Stream Approach (Declarative)

Look at how incredibly readable this becomes! It reads almost like plain English.

import java.util.List;
import java.util.stream.Collectors;

public class LabStream2 {

public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Amanda", "Charlie", "Adam");

List<String> result = names
.stream() // 1. SOURCE
.filter((name) -> name.startsWith("A")) // 2. INTERMEDIATE: Filter
.map((name) -> name.toUpperCase()) // 3. INTERMEDIATE: Map
.sorted() // 4. INTERMEDIATE: Sort
.collect(Collectors.toList()); // 5. TERMINAL: Collect

System.out.println(result);
}
}