Stream API

⭐ Interview Importance: MEDIUM
⏱️ Revision Time: 5 min

TL;DR

  • The Stream API (java.util.stream) was introduced in Java 8 to process collections of objects in a functional, declarative style.
  • Streams do not store data; they take data from a source (like a Collection), process it through a pipeline of operations, and output a result.
  • A Stream pipeline consists of a Source, zero or more Intermediate Operations (like filter, map), and one Terminal Operation (like collect, forEach).

Concept

Before Java 8, manipulating data in a List (e.g., finding all active users, getting their emails, and sorting them) required verbose for loops, if statements, and temporary lists. This is Imperative Programming (telling the computer how to do it).

Streams allow Declarative Programming (telling the computer what you want). You create a pipeline of functional operations.

Streams have two distinct phases:

  1. Intermediate Operations: Return a new Stream. They are Lazy, meaning they don’t actually execute until a Terminal Operation is called.
  2. Terminal Operations: Return a final result (or void) and close the Stream. A stream can only be consumed once.

Examples

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

public class StreamExample {
    public static void main(String[] args) {
        List<String> items = List.of("apple", "banana", "cherry", "avocado", "blueberry");
        
        // Goal: Get all fruits starting with 'a', convert them to uppercase, and put them in a list.
        
        // --- PRE-JAVA 8 WAY ---
        /*
        List<String> result = new ArrayList<>();
        for (String item : items) {
            if (item.startsWith("a")) {
                result.add(item.toUpperCase());
            }
        }
        */
        
        // --- JAVA 8 STREAM API ---
        List<String> result = items.stream()           // 1. Source
            .filter(s -> s.startsWith("a"))            // 2. Intermediate (Predicate)
            .map(s -> s.toUpperCase())                 // 3. Intermediate (Function)
            .collect(Collectors.toList());             // 4. Terminal
            
        System.out.println(result); // [APPLE, AVOCADO]
    }
}

Interview Questions

Q: Can you reuse a Stream once a terminal operation has been called on it?
A: No. Streams are single-use. Once a terminal operation (like collect, count, or forEach) is executed, the stream is considered “consumed” and closed. If you try to call another operation on the same stream reference, it will throw an IllegalStateException: stream has already been operated upon or closed. You must create a new stream from the source collection.

Q: What does it mean that Intermediate Operations are “Lazy”?
A: Laziness means that when you write stream.filter(...).map(...), no filtering or mapping actually happens yet. Java simply builds an internal blueprint (a pipeline) of the instructions. The data only starts flowing through the pipeline the moment the Terminal Operation is invoked. This allows Java to heavily optimize the execution under the hood (e.g., short-circuiting).