Introduction To Java Streams API

ggorantala

Gopi Gorantala

Posted on March 7, 2022

Introduction To Java Streams API

Before reading this, I would recommend trying and learning what are imperative and declarative styles of programming.

Ref - Dev.to Imperative vs Declarative

One of the powerful features the Java team included when bundling JDK 1.8 is the Streams API.

The purpose of streams is to process a sequence of elements by executing different kinds of operations on the elements.

New Java package for streams is java.util.stream.

What is a Stream?

Let us see some of the important aspects of Streams API.

  1. Streams API is used to process collections of objects.

  2. No Storage. A Stream is not a data structure like Arrays/Stack/Queue etc instead it takes input from Collections, Arrays, or I/O channels.

  3. Streams don’t change the original data structure, they only provide results as per the pipelined methods.

  4. Streams are used to perform complex data processing operations on collections data. They represent a pipeline through which the data will flow and the functions to operate on the data.

  5. In streams, we make use of Lambda expressions to improve the collection library, by making it easier to iterate through the filter and extract data from a collection using Streams.

  6. Any given stream can potentially have an unlimited amount of data flowing through it, data received from a stream is processed individually as it arrives.

  7. Functional in nature. An operation on a stream produces a result but does not modify its source. For example, filtering a Stream obtained from a collection produces a new Stream without the filtered elements, rather than removing elements from the source collection.

  8. Laziness-seeking. Many stream operations, such as filtering, mapping, or duplicate removal, can be implemented lazily, exposing opportunities for optimization. For example, “find the first String with three consecutive vowels” need not examine all the input strings. Stream operations are divided into intermediate (Stream-producing) operations and terminal (value- or side-effect-producing) operations. Intermediate operations are always lazy.

  9. Possibly unbounded. While collections have a finite size, streams need not. Short-circuiting operations such as limit(n) or findFirst() can allow computations on infinite streams to complete in finite time.

  10. Consumable. The elements of a stream are only visited once during the life of a stream. Like an Iterator, a new stream must be generated to revisit the same elements of the source.

Creating Streams

We have a few different ways to create a stream.

  1. From Collections.

  2. From arrays.

  3. From an arbitrary number of objects.

  4. Infinite and finite streams.

Benefits

Following are the advantages of using Streams API.

  1. We have already seen the difference between imperative and declarative approaches. With Streams, we move from imperative to declarative programming.

  2. Streams can only be applied to Collections such as List, Map, Set, Arrays, etc.

  3. Streams encourage less mutability

  4. They can operate on their contents in parallel using the parallelStream().

  5. Another benefit/advantage is streams process data lazily. Let us assume we are reading lines of text from a large file as a stream, which means the stream loads data as required which saves a lot of memory.

Stream Pipeline

Enough talk! Let us see an example 🤩

Let us see an example to understand the stream pipeline.

We have a list of the books and we are running some chain of methods on it like filter, map, etc. This chain of method calls is called a Stream Pipeline.

This is also called functional programming as we are passing functions.

Book is a POJO with a constructor, getters, and setters.

class Book {
  String title;
  String author;
  Integer year;
  Integer copiesSoldInMillions;
  Double rating;
  Double costInEuros;

  public Book(String title, String author, Integer year, Integer copiesSoldInMillions, Double rating, Double costInEuros) {
    this.title = title;
    this.author = author;
    this.year = year;
    this.copiesSoldInMillions = copiesSoldInMillions;
    this.rating = rating;
    this.costInEuros = costInEuros;
  }

  public String getAuthor() {
    return author;
  }

  public Double getRating() {
    return rating;
  }

  @Override
  public String toString() {
    return "Book{" +
      "title='" + title + '\'' +
      ", author='" + author + '\'' +
      ", year=" + year +
      ", copiesSoldInMillions=" + copiesSoldInMillions +
      ", rating=" + rating +
      ", costInEuros=" + costInEuros +
      '}';
  }
}
Enter fullscreen mode Exit fullscreen mode

Another class BookDatabase for dummy data injection.

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

public class BookDatabase {
  public static List<Book> getAllBooks() {
    return Arrays.asList(
      new Book("Don Quixote", "Miguel de Cervantes", 1605, 500, 3.9, 9.99),
      new Book("A Tale of Two Cities", "Charles Dickens", 1859, 200, 3.9, 10.0),
      new Book("The Lord of the Rings", "J.R.R. Tolkien", 2001, 150, 4.0, 12.50),
      new Book("The Little Prince", "Antoine de Saint-Exupery", 2016, 142, 4.4, 5.0),
      new Book("The Dream of the Red Chamber", "Cao Xueqin", 1791, 100, 4.2, 10.0)
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

And finally out BookApplication class that does the declarative programming or immutability on each book object.

import java.util.List;

public class BookApplication {
  public static void main(String[] args) {
    List<Book> books = BookDatabase.getAllBooks();

    books.stream()
      .filter(book -> book.getRating() >= 4)
      .map(Book::getAuthor)
      .forEach(System.out::println);
  }
}
Enter fullscreen mode Exit fullscreen mode

Output:

J.R.R. Tolkien
Antoine de Saint-Exupery
Cao Xueqin
Enter fullscreen mode Exit fullscreen mode

In the above example, we have a list of Book objects.

  1. We applied .stream(...) to convert the list of Book objects into a stream of objects.

  2. We applied two intermediate operations .filter(...) that takes a Predicate and .map(...) to get the filtered stream of objects.

  3. We terminated it with .forEach(...)to print all the objects.

Disadvantages

  1. If streams are not handled properly, they have a huge impact on performance.

  2. The learning curve is time taking, as there are many overloaded methods you need to learn which is confusing, not a disadvantage though but wanna keep it here as a downside.

Read More

  1. How Does Streams API Work? A Deep Dive on Stream Operation Flow

  2. What Are Java Method References And Kinds Of Method References Available?

  3. Functional Programming And Programming Paradigms in Java

  4. Imperative And Declarative Styles Of Programming

💖 💪 🙅 🚩
ggorantala
Gopi Gorantala

Posted on March 7, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related