Java Stream API

Java Stream API: Complete Guide for Beginners and Experts

Java Stream API

Learn the Java Stream API with simple explanations and practical examples. This guide covers what Stream API is, the difference between Collection and Stream, different ways to create a Stream, intermediate operations, terminal operations, and lazy evaluation.

What is Stream API in Java?

A Stream is a sequence of elements that allows us to perform operations on those elements in a functional and declarative way.

A Stream is not a data structure. It is a pipeline used to process data from a source such as:

  • Collections
  • Arrays
  • Maps
  • Files
  • Generated values

What is the Difference Between Collection and Stream?

  1. A Collection is used to store and manage data, whereas a Stream is used to process data.
  2. A Collection holds the actual elements, whereas a Stream does not store elements.
  3. A Collection can generally be traversed multiple times, whereas a Stream can generally be consumed only once.
  4. A Collection can be modified by adding or removing elements, whereas Stream operations do not modify the source collection by default.
  5. A Collection itself does not provide a pipeline of data-processing operations, whereas a Stream provides a pipeline of intermediate and terminal operations.
  6. Collection operations are generally eager, whereas Stream intermediate operations are lazy.

How Do You Create a Stream in Java?

There are several ways to create a Stream in Java.

1. Using Collection


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

Stream stream = names.stream();

2. Using Array


String[] names = {"Amit", "Rahul", "Deep"};

Stream stream = Arrays.stream(names);

3. Using Primitive Streams


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

LongStream.range(1, 10).forEach(System.out::println);

DoubleStream.of(1.0, 2.0, 3.0).forEach(System.out::println);

4. Using Stream.of()


Stream<String> stream = Stream.of("Amit", "Rahul", "Deep");

5. Using an Empty Stream


Stream<String> stream = Stream.empty();

6. Using Stream Builder

Stream<String> stream = Stream.<String>builder()
    .add("Amit")
    .add("Rahul")
    .add("Deep")
    .build();

7. Using Stream.generate()


Stream<Integer> stream = Stream.generate(() -> 10);

Note: Stream.generate() creates an infinite Stream by default, so it is commonly used with operations such as limit().

8. Using Stream.iterate()


Stream<Integer> stream = Stream.iterate(1, n -> n + 1);

Note: This also creates an infinite Stream unless a limiting operation such as limit() is applied.

9. Using Files


Stream<String> lines = Files.lines(Path.of("customer.txt"));

When reading a file using Files.lines(), it is recommended to use try-with-resources so that the Stream and underlying file resources are properly closed.


try (Stream<String> lines = Files.lines(Path.of("customer.txt"))) {
lines.forEach(System.out::println);
}

What are Intermediate and Terminal Operations?

In the Java Stream API, operations are mainly divided into two types:

  1. Intermediate Operations
  2. Terminal Operations

Intermediate Operations in Java Stream API

Intermediate operations transform, filter, or otherwise process elements of a Stream and return another Stream. They are lazy, meaning they are not executed until a terminal operation is called.


List<Integer> numbers = Arrays.asList(10, 20, 20, 30, 40);

Stream<Integer> stream = numbers.stream()
.filter(n -> n > 10)
.distinct()
.map(n -> n * 2);

In the above example, filter(), distinct(), and map() are intermediate operations.

Common Intermediate Operations

  • filter() – Filters elements based on a given condition and keeps only the elements that satisfy it.
  • map() – Transforms each element into another value or type.
  • flatMap() – Flattens nested collections or Streams into a single Stream of elements.
  • distinct() – Removes duplicate elements from the Stream.
  • sorted() – Sorts Stream elements according to their natural ordering.
  • sorted(Comparator) – Sorts Stream elements according to a custom sorting rule.
  • limit() – Restricts the Stream to a maximum number of elements.
  • skip() – Skips the specified number of elements from the beginning of the Stream.
  • peek() – Performs an action on elements as they pass through the Stream, mainly for debugging or inspection.
  • mapToInt() – Converts elements into an IntStream for primitive integer processing.
  • mapToLong() – Converts elements into a LongStream for primitive long processing.
  • mapToDouble() – Converts elements into a DoubleStream for primitive double processing.
  • unordered() – Removes the encounter-order requirement when the Stream does not need to preserve ordering.

Important: Nothing is actually processed at this point because there is no terminal operation.

Terminal Operations in Java Stream API

Terminal operations produce a final result or side effect and terminate the Stream. Once a terminal operation is executed, the Stream is consumed and cannot normally be reused.


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

List<Integer> result = numbers.stream()
.filter(n -> n > 20)
.map(n -> n * 2)
.sorted()
.collect(Collectors.toList());

Common Terminal Operations

  • forEach() – Performs an action for each element in the Stream.
  • forEachOrdered() – Performs an action for each element while maintaining the Stream's encounter order.
  • collect() – Collects Stream elements into a collection or another result container.
  • toArray() – Converts Stream elements into an array.
  • reduce() – Combines Stream elements into a single result using an accumulation operation.
  • count() – Returns the total number of elements in the Stream.
  • min() – Returns the minimum element according to the given comparator.
  • max() – Returns the maximum element according to the given comparator.
  • findFirst() – Returns the first element of the Stream as an Optional.
  • findAny() – Returns any element of the Stream as an Optional, especially useful with parallel Streams.
  • anyMatch() – Returns true if at least one element satisfies the given condition.
  • allMatch() – Returns true if every element satisfies the given condition.
  • noneMatch() – Returns true if no element satisfies the given condition.
  • iterator() – Returns an iterator for traversing the Stream elements.
  • spliterator() – Returns a Spliterator that can traverse and potentially split the Stream elements.