A Comprehensive Explanation of Java Streams

Photo by qi xna on Unsplash

A Comprehensive Explanation of Java Streams

Explain how Java Stream work with code example.

·

4 min read

Introduced as part of the major features in Java 8, Java Stream quickly became one of the most beloved features. It defines a functional-style that helps developers integrate with data structures such as arrays and collections more easily and efficiently. In this article, we will get to know what Java Stream is and how it works.

What is Streams?

Java Streams are mainly based on the java.util.stream.Stream interface, which is part of the Java 8 APIs. This interface allows programmers to pass data to the Stream, which it then processes using its various capabilities like sorting, filtering or mapping. By combining multiple Streams, one-by-one, complex pipelines can be created. In Java, these pipelines are known as Stream pipelines and can be used to carry out a series of tasks on the data that was passed as arguments to the Stream interface.

Furthermore, Streams can be used in parallel, meaning that multiple Streams can be run at the same time, enhancing the performance of the application.

Intermediate operations and Terminal operations

In Java Stream, every Stream process must be run through two operations to finish and conclude the handling of data. The first is an Intermediate operation, and the second is a Terminal operation.

Intermediate operations are used to pass data, handle logic, and return another stream. As their name implies, these operations do not return the final result; rather, they return a stream pipeline to process data.

Intermediate operations are executed lazily. This means that the operations will not run until a terminal operation is called. This behavior improves the overall performance of Java Streams.

Some common used of intermediate operations are:

  • filter: Filter out the data based on condition

  • map: Transform each element of data into other type

  • sorted: Sort items on the stream

  • distinct: Remove duplicate elements

While Intermediate operations act as way to input and processing data, Terminal operations are used for produce the final result for the Stream operations. When we call terminal operation, the operation will trigger other intermediate operations to execute and return the final result.

Some common used of Terminal operations are:

  • forEach: Perform specific action on each element of Stream

  • min: Return minimum element of Stream

  • max: Return maximum element of Stream

  • anyMatch: Return boolean based on any elements that match the condition

  • allMatch: Return boolean based on all elements that match the condition

  • noneMatch: Return boolean based on no elements that match the condition

  • collect: Convert elements in Stream to collection such as List or Set

Java Stream characteristics

Java Streams are highlighted for it following characteristics:

  • Lazily evaluated: meaning that the handling of element logic is only processed when a terminal operation is called, allowing for optimizations such as short-circuiting and fusion.

  • Designed with a functional-programming approach: Java Streams feature functional interfaces in Java 8. This means that operations can be composed of operations, that they are immutable, and that they are stateless.

  • Executed only once: Once the operation of a Stream is executed, it cannot be used again. If you want to take further action on a Stream, you have to create a new one.

  • Parallel execution: Java Streams support executing operations on multiple threads, which improves performance when dealing with large amounts of data.

  • Pipelined: allowing for complex processing of elements with each operation performing a specific task on the elements in the stream.

Example of using Java Stream

Imagine you are writing a Programm that receive list of numbers, it will then filter out even numbers and then sort the list in the increase order. In normal Java approach the code will look like this:

List<Integer> numbers = Arrays.asList(2,3,1,9,7,6,5,4);
List<Integer> filteredNumbers = new ArrayList<>();

for (int number : numbers) {
      if (number % 2 != 0) {
          filteredNumbers.add(number);
      }
}

Collections.sort(filteredNumbers);

This approach still work and the result correct as we expected but the code look verbose with many variables we have to create in order to catch the value.

Let’s see the more declarative and concise approach in Java Stream:

List<Integer> numbers = Arrays.asList(2,3,1,9,7,6,5,4);
List<Integer> filteredNumbers = numbers.stream()
.filter(number -> number % 2 != 0)
.sorted()
.collect(Collectors.toList());

Clearly, the second example is more clear and concise, requiring less boilerplate code and making it easier to comprehend.

Summary

Java Stream is one of the most powerful features introduced since Java 8. It provides a simple and efficient way to process data elements in a pipeline of operations, and they can be used to perform complex data processing tasks in a concise and readable manner.

Understanding and managing to use Java Streams correctly can help developers reduce a significant amount of boilerplate code, boost program performance, and create more readable code.