Master Core Java Programming From Scratch

Clear, interactive, and structured coding lessons designed for absolute beginners.

Stream API in Java

Learn the Java Stream API for processing collections and data using filtering, mapping, sorting, reducing, grouping, matching, and collecting operations.

What is Stream API?

The Stream API provides a functional-style approach for processing sequences of data.

A stream does not store data. Instead, it represents a pipeline of operations performed on a data source such as a collection, array, or generated sequence.

Concept
Data Source
     ↓
Stream
     ↓
Intermediate Operations
     ↓
Terminal Operation
     ↓
Result

Creating a Stream

Java
List<String> names =
    List.of(
        "Amit",
        "Neha",
        "Rahul"
    );

names.stream()
    .forEach(
        name ->
            System.out.println(name)
    );

Stream Pipeline

A stream pipeline normally contains a source, zero or more intermediate operations, and one terminal operation.

Java
numbers.stream()
    .filter(number -> number > 10)
    .map(number -> number * 2)
    .forEach(
        System.out::println
    );
  • stream() creates the stream.
  • filter() selects elements.
  • map() transforms elements.
  • forEach() consumes the result.

Intermediate Operations

Intermediate operations transform or refine a stream and return another stream.

Operation Purpose
filter() Selects elements matching a condition.
map() Transforms each element.
flatMap() Flattens nested stream structures.
distinct() Removes duplicate elements.
sorted() Sorts elements.
limit() Restricts the number of elements.
skip() Skips the first elements.
peek() Allows an action for debugging/observation.

Terminal Operations

Terminal operations produce a result or side effect and consume the stream.

Operation Purpose
forEach() Performs an action for each element.
collect() Collects results into a data structure.
reduce() Combines elements into a single result.
count() Counts elements.
min() Finds the minimum element.
max() Finds the maximum element.
findFirst() Returns the first element if present.
anyMatch() Checks whether any element matches.

filter()

The filter() operation keeps elements that satisfy a condition.

Java
List<Integer> numbers =
    List.of(
        10,
        15,
        20,
        25,
        30
    );

numbers.stream()
    .filter(
        number -> number > 20
    )
    .forEach(
        System.out::println
    );

map()

The map() operation transforms each stream element into another value.

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

numbers.stream()
    .map(
        number -> number * number
    )
    .forEach(
        System.out::println
    );

distinct()

The distinct() operation removes duplicate elements according to the stream elements' equality.

Java
List<Integer> numbers =
    List.of(
        10,
        20,
        10,
        30,
        20
    );

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

sorted()

The sorted() operation returns elements in sorted order.

Java
List<Integer> numbers =
    List.of(
        50,
        10,
        40,
        20,
        30
    );

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

Sorting with Comparator

Java
List<String> names =
    List.of(
        "Amit",
        "Neha",
        "Rahul",
        "Priya"
    );

names.stream()
    .sorted(
        (a, b) ->
            b.compareTo(a)
    )
    .forEach(
        System.out::println
    );

limit()

The limit() operation restricts the stream to a maximum number of elements.

Java
List<Integer> numbers =
    List.of(
        10,
        20,
        30,
        40,
        50
    );

numbers.stream()
    .limit(3)
    .forEach(
        System.out::println
    );

skip()

The skip() operation discards the first specified number of elements.

Java
numbers.stream()
    .skip(2)
    .forEach(
        System.out::println
    );

flatMap()

flatMap() is useful when each element produces another collection or stream and you want one flattened stream.

Java
List<List<String>> groups =
    List.of(
        List.of("A", "B"),
        List.of("C", "D")
    );

groups.stream()
    .flatMap(
        group -> group.stream()
    )
    .forEach(
        System.out::println
    );

reduce()

The reduce() operation combines stream elements into a single result.

Java
List<Integer> numbers =
    List.of(10, 20, 30, 40);

int sum =
    numbers.stream()
        .reduce(
            0,
            (a, b) -> a + b
        );

System.out.println(sum);

count()

Java
long count =
    numbers.stream()
        .filter(
            number -> number > 20
        )
        .count();

System.out.println(count);

min() and max()

Java
Optional<Integer> minimum =
    numbers.stream()
        .min(Integer::compareTo);

Optional<Integer> maximum =
    numbers.stream()
        .max(Integer::compareTo);

Matching Operations

Streams provide matching operations for checking conditions across elements.

Java
boolean any =
    numbers.stream()
        .anyMatch(
            number -> number > 30
        );

boolean all =
    numbers.stream()
        .allMatch(
            number -> number > 0
        );

boolean none =
    numbers.stream()
        .noneMatch(
            number -> number < 0
        );
Operation Meaning
anyMatch() At least one element matches.
allMatch() Every element matches.
noneMatch() No element matches.

findFirst() and findAny()

Java
Optional<Integer> first =
    numbers.stream()
        .filter(
            number -> number > 20
        )
        .findFirst();

first.ifPresent(
    System.out::println
);

The result is wrapped in Optional because the stream may contain no matching element.

collect()

The collect() operation gathers stream results into collections or other result containers.

Java
List<Integer> evenNumbers =
    numbers.stream()
        .filter(
            number -> number % 2 == 0
        )
        .collect(
            Collectors.toList()
        );

Collecting with toList()

Java
List<String> result =
    names.stream()
        .filter(
            name -> name.length() > 4
        )
        .collect(
            Collectors.toList()
        );

Collecting with toSet()

Java
Set<String> uniqueNames =
    names.stream()
        .collect(
            Collectors.toSet()
        );

Collectors.joining()

joining() can combine strings into a single string.

Java
String result =
    names.stream()
        .collect(
            Collectors.joining(", ")
        );

System.out.println(result);

groupingBy()

groupingBy() groups elements according to a classification function.

Java
Map<Integer, List<String>> grouped =
    names.stream()
        .collect(
            Collectors.groupingBy(
                String::length
            )
        );

partitioningBy()

partitioningBy() divides elements into two groups based on a boolean condition.

Java
Map<Boolean, List<Integer>> partitioned =
    numbers.stream()
        .collect(
            Collectors.partitioningBy(
                number -> number % 2 == 0
            )
        );

peek()

peek() can be useful for observing elements while debugging a stream pipeline. It should not normally be used as the main mechanism for application side effects.

Java
numbers.stream()
    .filter(
        number -> number > 10
    )
    .peek(
        number ->
            System.out.println(
                "Before map: " + number
            )
    )
    .map(
        number -> number * 2
    )
    .forEach(
        System.out::println
    );

Lazy Evaluation

Intermediate operations are lazy. They are not generally executed until a terminal operation is invoked.

Java
Stream<Integer> stream =
    numbers.stream()
        .filter(
            number -> number > 10
        );

System.out.println(
    "Pipeline created"
);

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

Streams Cannot Normally Be Reused

A stream is intended for a single pipeline execution. After a terminal operation consumes it, attempting to reuse the same stream results in an exception.

Java
Stream<String> stream =
    names.stream();

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

// Do not reuse the same stream here.

Parallel Streams

A parallel stream can process elements using multiple threads. It can be useful for suitable CPU-intensive workloads, but it should not be assumed to be faster for every task.

Java
numbers.parallelStream()
    .filter(
        number -> number > 10
    )
    .forEach(
        System.out::println
    );
Parallel streams require careful consideration of workload, ordering, shared state, thread safety, and performance.

Stream and Optional

Operations such as findFirst(), findAny(), min(), and max() commonly return Optional because a result may not exist.

Java
Optional<Integer> result =
    numbers.stream()
        .filter(
            number -> number > 100
        )
        .findFirst();

result.ifPresent(
    System.out::println
);

Primitive Streams

Java provides specialized streams for primitive numeric types: IntStream, LongStream, and DoubleStream.

Java
IntStream.range(
        1,
        6
    )
    .forEach(
        System.out::println
    );

Numeric Stream Operations

Java
IntSummaryStatistics statistics =
    numbers.stream()
        .mapToInt(
            Integer::intValue
        )
        .summaryStatistics();

System.out.println(
    statistics.getCount()
);

System.out.println(
    statistics.getSum()
);

System.out.println(
    statistics.getAverage()
);

System.out.println(
    statistics.getMin()
);

System.out.println(
    statistics.getMax()
);

Example 👨‍🏫🤓

Suppose an application needs to find active employees whose salary is above a particular threshold and return their names.

Java
List<Employee> employees =
    getEmployees();

List<String> result =
    employees.stream()
        .filter(
            employee ->
                employee.isActive()
        )
        .filter(
            employee ->
                employee.getSalary() > 50000
        )
        .map(
            Employee::getName
        )
        .sorted()
        .collect(
            Collectors.toList()
        );

Benefits of Stream API

  • Provides concise data-processing pipelines.
  • Works naturally with lambda expressions.
  • Supports filtering, mapping, sorting, grouping, and reduction.
  • Encourages declarative programming.
  • Supports sequential and parallel processing models.

Stream API Best Practices

  • Keep stream pipelines readable.
  • Avoid unnecessary stream operations.
  • Prefer method references when they improve readability.
  • Avoid modifying shared mutable state inside stream operations.
  • Use peek() mainly for debugging or observation.
  • Use parallel streams only after considering workload and thread-safety implications.

Interview Questions

Stream API provides a functional-style approach for processing sequences of data.

Intermediate operations return another stream and are generally lazy. Terminal operations consume the stream and produce a result or side effect.

Intermediate stream operations are generally not executed until a terminal operation starts consuming the pipeline.

filter() selects elements based on a condition, while map() transforms elements into another form.

A stream is intended for a single pipeline execution. After a terminal operation, the same stream should not be reused.

reduce() combines stream elements into a single result using an accumulator operation.

flatMap() transforms each element into a stream and then flattens those streams into one stream.

No. Performance depends on the workload, data size, operation costs, hardware, ordering requirements, and thread-safety characteristics.
Summary

The Java Stream API provides a powerful way to process collections and other data sources using declarative pipelines. Important operations include filter, map, flatMap, distinct, sorted, limit, skip, reduce, collect, groupingBy, matching operations, and primitive streams.