Java Tutorial 0/145 lessons ~6 min read Lesson 41

    Streams API

    java streams API filter map flatMap collect reduce parallelStream lazy evaluation terminal intermediate operations

    Course progress0%
    Focus
    30 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    In the previous lessons, you learned about the Java Collections Framework, including List, Set, Map, and other collection types. Collections allow us to store and manage groups of objects efficiently.

    However, consider the following common tasks:

    • Find all employees with a salary greater than ₹100,000.
    • Sort products by price.
    • Count active users.
    • Convert names to uppercase.
    • Find duplicate records.
    • Group employees by department.
    • Calculate the average salary.
    • Process millions of records efficiently.

    Before Java 8, these operations required multiple loops, temporary collections, and a significant amount of boilerplate code.

    Java 8 introduced the Streams API, which provides a declarative and functional approach to processing collections.

    Instead of describing how to process data, Streams focus on what result you want.

    The Streams API is extensively used in:

    • Spring Boot Applications
    • Microservices
    • REST APIs
    • Hibernate/JPA
    • Big Data Processing
    • Data Analytics
    • Financial Systems
    • Enterprise Java Applications

    Mastering Streams is essential for modern Java development and is one of the most frequently asked interview topics.

    In this lesson, you'll learn:

    • What is Streams API?
    • Why Streams?
    • Stream Pipeline
    • Creating Streams
    • Intermediate Operations
    • Terminal Operations
    • Lazy Evaluation
    • Functional Programming
    • Parallel Streams
    • Performance Considerations
    • Best Practices
    • Common Mistakes
    • Hands-on Exercises
    • Professional Interview Questions

    What is a Stream?

    A Stream is a sequence of elements that supports functional-style operations to process data.

    Unlike collections:

    • Collections store data.
    • Streams process data.

    Example:

    code
    List<String> names = List.of(
    "Rahul",
    "Amit",
    "John"
    );
    names.stream()
    .forEach(System.out::println);

    Output:

    code
    Rahul
    Amit
    John

    Why Streams?

    Before Java 8:

    code
    List<String> result =
    new ArrayList<>();
    for(String name : names){
    if(name.startsWith("R")){
    result.add(name);
    }
    }

    Using Streams:

    code
    names.stream()
    .filter(name -> name.startsWith("R"))
    .forEach(System.out::println);

    Benefits:

    • Less code
    • Better readability
    • Functional programming style
    • Easier parallel processing
    • Reduced bugs

    Stream Pipeline

    A Stream consists of three stages.

    code
    Source
    Intermediate Operations
    Terminal Operation

    Example:

    code
    numbers.stream()
    .filter(n -> n > 10)
    .map(n -> n * 2)
    .collect(Collectors.toList());

    Creating Streams

    From Collection

    code
    List<String> list =
    List.of("A","B","C");
    Stream<String> stream =
    list.stream();

    From Array

    code
    String[] names = {
    "Java",
    "Spring",
    "Kafka"
    };
    Arrays.stream(names)
    .forEach(System.out::println);

    Using Stream.of()

    code
    Stream.of(10,20,30,40)
    .forEach(System.out::println);

    Infinite Streams

    code
    Stream.generate(Math::random)
    .limit(5)
    .forEach(System.out::println);

    Intermediate Operations

    Intermediate operations return another Stream.

    They are lazy, meaning they do not execute until a terminal operation is invoked.

    Common intermediate operations:

    • filter()
    • map()
    • flatMap()
    • sorted()
    • distinct()
    • limit()
    • skip()
    • peek()

    filter()

    Filters elements based on a condition.

    code
    List<Integer> numbers =
    List.of(10,20,30,40);
    numbers.stream()
    .filter(n -> n > 20)
    .forEach(System.out::println);

    Output:

    code
    30
    40

    map()

    Transforms each element.

    code
    List<String> names =
    List.of("java","spring");
    names.stream()
    .map(String::toUpperCase)
    .forEach(System.out::println);

    Output:

    code
    JAVA
    SPRING

    flatMap()

    Converts nested collections into a single stream.

    code
    List<List<String>> data =
    List.of(
    List.of("A","B"),
    List.of("C","D")
    );
    data.stream()
    .flatMap(List::stream)
    .forEach(System.out::println);

    Output:

    code
    A
    B
    C
    D

    sorted()

    Sorts elements.

    code
    List<Integer> numbers =
    List.of(5,2,9,1);
    numbers.stream()
    .sorted()
    .forEach(System.out::println);

    Output:

    code
    1
    2
    5
    9

    distinct()

    Removes duplicates.

    code
    List<Integer> numbers =
    List.of(1,2,2,3,3,4);
    numbers.stream()
    .distinct()
    .forEach(System.out::println);

    Output:

    code
    1
    2
    3
    4

    limit() and skip()

    code
    Stream.of(1,2,3,4,5)
    .limit(3)
    .forEach(System.out::println);

    Output:

    code
    1
    2
    3
    code
    Stream.of(1,2,3,4,5)
    .skip(2)
    .forEach(System.out::println);

    Output:

    code
    3
    4
    5

    peek()

    Used mainly for debugging.

    code
    numbers.stream()
    .peek(System.out::println)
    .filter(n -> n > 20)
    .forEach(System.out::println);

    Avoid using peek() for business logic.

    Terminal Operations

    Terminal operations trigger stream execution.

    Common terminal operations:

    • forEach()
    • collect()
    • count()
    • reduce()
    • findFirst()
    • findAny()
    • anyMatch()
    • allMatch()
    • noneMatch()
    • min()
    • max()

    collect()

    Collects results into a collection.

    code
    List<String> result =
    names.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toList());

    count()

    code
    long total =
    numbers.stream()
    .filter(n -> n > 10)
    .count();

    reduce()

    Reduces elements into a single result.

    code
    int sum =
    Stream.of(10,20,30)
    .reduce(0, Integer::sum);
    System.out.println(sum);

    Output:

    code
    60

    findFirst()

    code
    Optional<String> name =
    names.stream()
    .findFirst();

    Match Operations

    code
    numbers.stream()
    .anyMatch(n -> n > 100);

    Other operations:

    • allMatch()
    • noneMatch()

    Lazy Evaluation

    Streams execute only when a terminal operation is called.

    code
    numbers.stream()
    .filter(n -> {
    System.out.println(n);
    return n > 20;
    });

    Nothing is printed because no terminal operation exists.

    Adding:

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

    Triggers execution.

    Parallel Streams

    Parallel Streams process data using multiple threads.

    code
    numbers.parallelStream()
    .forEach(System.out::println);

    Suitable for:

    • Large datasets
    • CPU-intensive computations

    Avoid parallel streams for small collections or operations with side effects.

    Stream vs Collection

    CollectionStream
    Stores dataProcesses data
    Can be modifiedCannot modify source
    Multiple traversalsSingle traversal
    EagerLazy

    Real-World Example: Employee Filtering

    code
    employees.stream()
    .filter(emp -> emp.getSalary() > 100000)
    .map(Employee::getName)
    .sorted()
    .forEach(System.out::println);

    Real-World Example: Product Price Calculation

    code
    double total =
    products.stream()
    .map(Product::getPrice)
    .reduce(0.0, Double::sum);

    Best Practices

    Prefer Streams for Collection Processing

    Streams improve readability for filtering, mapping, grouping, and aggregation.

    Keep Stream Operations Stateless

    Avoid modifying external variables inside stream operations.

    Use Method References When Appropriate

    Prefer:

    code
    .map(String::toUpperCase)

    Instead of:

    code
    .map(s -> s.toUpperCase())

    when readability improves.

    Avoid Parallel Streams by Default

    Measure performance before introducing parallelism.

    Do Not Reuse Streams

    A Stream can be consumed only once.

    Common Mistakes

    Reusing a Stream

    code
    Stream<String> stream =
    names.stream();
    stream.count();
    stream.forEach(System.out::println);

    Output:

    code
    IllegalStateException

    Using Streams for Side Effects

    Prefer pure transformations over modifying external state.

    Forgetting Terminal Operations

    Without a terminal operation, intermediate operations never execute.

    Overusing Parallel Streams

    Parallel execution is not always faster.

    Ignoring Optional

    Methods such as findFirst() return an Optional. Handle it safely instead of calling get() blindly.

    Hands-on Exercise

    Create a Java program that:

    1. Creates a list of employee objects.
    2. Filters employees with salaries greater than ₹100,000.
    3. Converts employee names to uppercase.
    4. Removes duplicate department names.
    5. Sorts employees by salary.
    6. Calculates the total salary using reduce().
    7. Finds the first employee in the list.
    8. Counts employees in a specific department.
    9. Demonstrates parallelStream().
    10. Uses collect() to create a new list.

    Summary

    The Java Streams API provides a declarative, functional approach to processing collections. By combining intermediate and terminal operations into pipelines, developers can write concise, readable, and maintainable code. Features such as lazy evaluation, method references, lambda expressions, and parallel streams make the Streams API one of the most powerful additions introduced in Java 8.

    Key Takeaways

    • Streams process data; they do not store it.
    • A stream pipeline consists of a source, intermediate operations, and a terminal operation.
    • Intermediate operations are lazy.
    • Terminal operations trigger execution.
    • filter() selects elements.
    • map() transforms elements.
    • flatMap() flattens nested structures.
    • collect() gathers results.
    • reduce() aggregates values.
    • distinct() removes duplicates.
    • parallelStream() enables parallel processing.
    • A stream can be consumed only once.

    Professional Interview Questions

    1What is the difference between a Collection and a Stream?

    Professional Answer

    A Collection is a data structure used to store and manage objects in memory, whereas a Stream is a sequence of elements used to process data from a source such as a collection, array, or file. Collections are reusable and support multiple traversals, while streams are designed for one-time traversal and functional-style processing.

    Follow-up Questions

    • Can a Stream modify the underlying collection?
    • Why can a Stream be consumed only once?

    Interview Tip: Remember: Collection → Stores Data, Stream → Processes Data.

    2What are Intermediate and Terminal Operations?

    Professional Answer

    Intermediate operations transform a stream and return another stream, enabling pipeline construction. They are lazy and do not execute immediately. Terminal operations, such as collect(), forEach(), and reduce(), trigger the execution of the pipeline and produce a result or side effect.

    Follow-up Questions

    • Why are intermediate operations lazy?
    • Name five terminal operations.

    Interview Tip: Without a terminal operation, a stream pipeline does not execute.

    3What is the difference between map() and flatMap()?

    Professional Answer

    map() transforms each element into another value, resulting in one output element for each input element. flatMap() is used when each input element can produce multiple output elements, such as flattening nested collections or arrays into a single stream.

    Follow-up Questions

    • When would you use flatMap()?
    • Can flatMap() reduce nested lists to a single stream?

    Interview Tip: A simple rule: map() → One-to-One Transformation, flatMap() → Many-to-One Flattening.

    Ready to mark this lesson complete?Track your journey across the entire course.