Java 8 Stream API

Senura Vihan Jayadeva
8 min readMar 4, 2023

Hello guys,today my article is about Stream API. Let’s explore how we can use the Stream API with some examples.

What is Stream API ?

The Java Stream API is a powerful feature added to Java 8 that enables you to process data in a functional and declarative way. It provides a mechanism to perform aggregate operations on collections of data, such as filtering, mapping, and reducing, in a concise and expressive way.

A stream is an abstraction of a non-mutable collection of functions applied in some order to the data. A stream is not a collection where you can store elements.

The Stream API provides a set of methods that allow you to filter, transform, aggregate, and sort the elements in a stream.

Some of the key features of the Stream API include:

  • Stream sources: Streams can be created from various sources, such as collections, arrays, or I/O channels.
  • Intermediate operations: These operations are used to transform a stream into another stream. Examples include filtering, mapping, and sorting.
  • Terminal operations: These operations are used to produce a result or side-effect, such as collecting the elements of a stream into a collection or iterating over the elements of a stream.
  • Parallel Processing: The Stream API allows you to perform operations on a stream in parallel, which can significantly speed up processing of large data sets on multi-core processors.
  • Non-Mutating Operations: The Stream API provides operations that do not modify the original data source, instead they return a new stream or collection.

Using the Stream API can simplify the process of working with collections and make your code more expressive and easier to understand. Plus, it can make your code more efficient by processing data in parallel, which means it can do things faster!

Sources:

A source is essentially the origin of the elements that will be processed in the stream. There are several types of sources in the Java Stream API:

Collections: You can create a stream from a collection such as a List, Set, or Map. For example, you can create a stream of integers from a List using the following code:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = numbers.stream();

Arrays: You can also create a stream from an array. For example, you can create a stream of strings from an array using the following code:

String[] words = {"hello", "world", "how", "are", "you"};
Stream<String> stream = Arrays.stream(words);

I/O Channels: You can create a stream from an input/output channel such as a file or network socket. For example, you can create a stream of lines from a file using the following code:

Path path = Paths.get("myfile.txt");
Stream<String> stream = Files.lines(path);

Streams: You can create a stream from another stream. This is useful when you want to perform additional operations on an existing stream. For example, you can create a stream of even numbers from an existing stream of integers using the following code:

Stream<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5).stream();
Stream<Integer> evenNumbers = numbers.filter(n -> n % 2 == 0);

These are just a few examples of the sources that are available in the Java Stream API. Once you have a stream, you can perform a variety of operations on it, such as filtering, mapping, sorting, and reducing.

Intermediate operations:

Intermediate operations in Java Stream API are operations that transform a stream into another stream. These operations do not produce a final result, but rather return a new stream that can be further processed by additional intermediate or terminal operations.

Here are some common intermediate operations in Java Stream API:

  1. filter(): This operation returns a new stream consisting of the elements that satisfy the given predicate.
  2. map(): This operation returns a new stream by applying a given function to each element of the stream.
  3. flatMap(): This operation returns a new stream by applying a given function that returns a stream to each element of the original stream, and then flattening the resulting streams into a single stream.
  4. distinct(): This operation returns a new stream consisting of distinct elements of the original stream, according to their natural order or a provided comparator.
  5. sorted(): This operation returns a new stream consisting of the elements of the original stream sorted according to their natural order or a provided comparator.
  6. limit(): This operation returns a new stream consisting of the first n elements of the original stream, where n is a given long value.
  7. skip(): This operation returns a new stream consisting of all elements of the original stream except the first n elements, where n is a given long value.

Intermediate operations in Java Stream API are lazy evaluated, meaning that they are not executed until a terminal operation is called on the stream.

Here is a simple example of using the filter() method in the Stream API to filter out even numbers from a list of integers:

import java.util.Arrays;
import java.util.List;

public class StreamFilterExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.toList();

System.out.println("Even numbers: " + evenNumbers);
}
}
Even numbers: [2, 4, 6, 8, 10]

Terminal operations:

Terminal operations in Java Stream API are operations that produce a final result or a side effect. When a terminal operation is called on a stream, the stream is consumed, and no further operations can be performed on it.

Here are some common terminal operations in Java Stream API:

  1. findFirst(): This method is used to return the first element in the stream, which is an Optional. If the stream is empty, then an empty Optional is returned.
  2. findAny(): This method is used to return any element in the stream, which is also an Optional. This method can be useful when we want to get any element from the stream without any specific order. If the stream is empty, then an empty Optional is returned.
  3. min(): This method is used to return the smallest element in the stream, based on the natural order of the elements. If the stream is empty, then an empty Optional is returned.
  4. max(): This method is used to return the largest element in the stream, based on the natural order of the elements. If the stream is empty, then an empty Optional is returned.
  5. forEach(): This operation performs an action for each element of the stream.
  6. toArray(): This operation returns an array containing the elements of the stream.
  7. reduce(): This operation performs a reduction on the elements of the stream using a given BinaryOperator and returns an Optional object that may or may not contain the result.
  8. collect(): This operation performs a mutable reduction on the elements of the stream using a given Collector and returns the result as a collection or other type.
  9. count(): This operation returns the count of elements in the stream as a long value.
  10. anyMatch(): This operation returns true if any element of the stream matches the given predicate, false otherwise.
  11. allMatch(): This operation returns true if all elements of the stream match the given predicate, false otherwise.
  12. noneMatch(): This operation returns true if no element of the stream matches the given predicate, false otherwise.

Terminal operations in Java Stream API are eager evaluated, meaning that they are executed immediately when called on the stream. They are also short-circuiting, meaning that they may not process the entire stream if a certain condition is met.

Here’s a simple example of using the forEach() method in the Stream API to print out the elements of a stream:

import java.util.Arrays;
import java.util.List;

public class StreamForEachExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Dave");

names.stream()
.forEach(name -> System.out.println("Hello, " + name + "!"));
}
}

Important Facts:

Once a Stream has been consumed or closed, you cannot reuse it again: The reason for this is that Streams are designed to be used once only, and after the Stream has been consumed or closed, its internal state is modified or destroyed, making it impossible to reuse the same Stream object.

For example, consider the following code:

List<String> words = Arrays.asList("apple", "banana", "cherry");
Stream<String> stream = words.stream();
stream.forEach(System.out::println);
// Attempt to reuse the same stream - this will throw an IllegalStateException
stream.forEach(System.out::println);

In this code, we create a Stream from a List of strings and then use the forEach() method to print each element of the Stream. After the Stream has been consumed, we attempt to reuse the same Stream object and call forEach() again, which will result in an IllegalStateException being thrown because the Stream has already been closed.

If you need to perform multiple operations on the same data source using a Stream, you can create a new Stream each time by calling the stream() method on the data source again. For example:

List<String> words = Arrays.asList("apple", "banana", "cherry");

Stream<String> stream1 = words.stream();
stream1.forEach(System.out::println);
Stream<String> stream2 = words.stream();
stream2.filter(w -> w.startsWith("a")).forEach(System.out::println);

In this code, we create a new Stream object stream2 by calling the stream() method on the words list again, and then we perform a filter operation on the new Stream object. This allows us to perform multiple operations on the same data source using Streams, without attempting to reuse a Stream object that has already been consumed or closed.

Stream operations can be parallelized: Many stream operations can be executed in parallel by using the parallel() method. However, this should only be done if the underlying data source can be split into multiple independent parts.

Stream operations can be short-circuited: Some stream operations, such as findFirst() and findAny(), can short-circuit the pipeline and return a result as soon as it is available.

Stream sources can be infinite: Streams can be created from infinite sources, such as random number generators or network connections. However, you should always limit or terminate these streams using operations like limit() or findFirst().

Stream pipelines should be stateless: Stream operations should not maintain any state outside of the stream pipeline. This means that operations like forEach() that have side effects should be used with caution.

Stream operations can be combined: Stream operations can be combined using the fluent API. This makes it easy to create complex stream pipelines that can be easily modified or extended.

Stream operations can be optimized: The Stream API includes many optimizations that can improve performance, such as short-circuiting and lazy evaluation. However, you should always benchmark your code to ensure that you are getting the best possible performance.

Why you might want to use:

So finally based on the content here are some reasons why you might want to use streams in Java:

  1. Improved readability and maintainability: Streams make your code more concise and easier to read, especially when dealing with complex data processing tasks.
  2. Efficiency: Streams can be optimized to process data in parallel, which can significantly improve performance for large datasets.
  3. Flexibility: Streams can be used to process any data source that implements the Iterable interface, including lists, arrays, and even files.
  4. Functional programming features: Streams provide a functional programming approach to processing data, which can make it easier to write code that is reusable and testable.
  5. Error handling: Streams provide a clean and concise way to handle errors and exceptions that may occur during data processing.

I guess now you have an high level idea about the Stream API. You can get a deep knowledge about Stream API by following below documentations.

https://www.baeldung.com/java-streams

https://www.java67.com/2016/04/java-8-stream-examples-and-tutorial.html

--

--

Senura Vihan Jayadeva

Software Engineering undergraduate of Sri Lanka Institute of Information Technology | Physical Science Undergraduate of University of Sri Jayewardenepura