Java Streams

Introduction to Streams

March 14, 2020

In java we heavily use Collection. Right?

We store our data in a suitable collection and then we keep on taking the data from the collection wherever needed, we may process the data or we use the data and most of the time we use iterators to get the data. So we keep on playing with the collection object, passing to one after another method.

Suppose we have a list of books, we want to know all the books that fall to a particular genre or say we want to know all the books that have a rating above 3.

For this What we will do we will take iterator and will keep on testing each book on the described criteria and will take out only ones we want.

The code we would be writing before Java 8 is like

//This is the bean class for Books
public class Book {
	
	//here we have

	private String name;
	private String Author;
	private String genre;
	private double rating;
	
	// a constructor
	
	public Book(String name, String Author, String genre, double rating) {
		this.name = name;
		this.Author = Author;
		this.genre = genre;
		this.rating = rating;
	}

	// getters and setters for these 4 fields
	
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getAuthor() {
		return Author;
	}
	public void setAuthor(String author) {
		Author = author;
	}
	public String getGenre() {
		return genre;
	}
	public void setGenre(String genre) {
		this.genre = genre;
	}
	public double getRating() {
		return rating;
	}
	public void setRating(double rating) {
		this.rating = rating;
	}

        //toString method
        @Override
	public String toString() {
		return "Book [name=" + name + ", Author=" + Author + ", genre=" + genre + ", rating=" + rating + "]";
	}
}
public static void main(String[] args) {

		List<Book> list = new ArrayList<>();

		List<Book> PopularHorrorBooks = new ArrayList<>();

		for (Book book : list) {

			// to filter out horror books

			if (book.getGenre().equalsIgnoreCase("Horror") && book.getRating() > 3) {
                       //add filtered out books.
				PopularHorrorBooks.add(book);
			}
		}

This code may seem simple but if we compare this to the code we would be writing in SQL, it would be more concise and readable.

SELECT name FROM books WHERE genre = 'horror' AND rating > 3

Can we have something in JAVA that can help process all the data declaratively and make code look more concise and readable like the SQL statement?

The answer is Streams.

Streams API is introduced in Java 8, which is used to process sequences of elements.

How does Stream work?

  1. The Stream takes the data from a source.
  2. Do all the processing
  3. Return the data into the container the user wants or just consume the data.
How Stream works?

A Stream is a sequence of elements from a source that supports data processing operations.

Declarative

In Streams, we need a source from which we get the data (the source can be anything: File, Arrays, Collections, I/O resources), and later we process the data and consume it. For example:

List<Book> horrorBooks = list.stream()
            .filter((book) -> book.getGenre().equalsIgnoreCase("Horror") )
            .collect(Collectors.toList());

In the above snippet, the source is List of Books, list.stream() returns the stream of elements that are going to be processed. The process is to filter out the books having Horror genre and to consume the data we collect it using collect(list).

It is more readable, concise and written in a single statement.

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class StreamIntoduction {

	public static void main(String[] args) {

		List<Book> books = new ArrayList<>();

                books.add(new Book("The Alchemist", "Paulo Coelho", "Adventure", 4.40));
		books.add(new Book("The Notebook", "Nicholas Sparks", "Romance", 4.10));
		books.add(new Book("Horror Cocktail", "Robert Bloch", "Horror", 2.67));
		books.add(new Book("House of Leaves", "Mark Z. Danielewski", "Horror", 4.10));

		List<Book> horrorBooks = new ArrayList<>();

		horrorBooks = books.stream()
            .filter((book) -> book.getGenre().equalsIgnoreCase("Horror") )
            .collect(Collectors.toList());

        //printing books
                horrorBooks.forEach(book -> System.out.println(book));
		}		
}
Book [name=Horror Cocktail, Author=Robert Bloch, genre=Horror, rating=2.67]
Book [name=House of Leaves, Author=Mark Z. Danielewski, genre=Horror, rating=4.1]

Flexible

The code is flexible as if you want to apply one more filter, add one filter call with another predicate.

List<Book> popularHorrorBooks = list.stream()
            .filter((book) -> book.getGenre().equalsIgnoreCase("Horror") )
            .filter((book) -> book.getRating() > 3) 
            .collect(Collectors.toList());

This looks like a pipeline of operations on the stream. Click here to understand the process in more detail.

Parallelizable

Sometimes to process a large collection of elements with the greater performance, we need to process it in parallel and writing parallel code is quite complicated, but not with Stream.

So another great benefit of using Stream is that we can go parallel using streams without any effort!

We just can add parallel() or ParallelStream().

If we have already created the stream, we can make it parallel by invoking parallel() method on it.

List<Book> popularHorrorBooks = list.stream().parallel()
            .filter((book) -> book.getGenre().equalsIgnoreCase("Horror") )
            .filter((book) -> book.getRating() > 3) 
            .collect(Collectors.toList());
		

Or we can directly create a parallel stream by invoking parallelStream() method on the list.

List<Book> popularHorrorBooks = list.parallelStream()
            .filter((book) -> book.getGenre().equalsIgnoreCase("Horror") )
            .filter((book) -> book.getRating() > 3) 
            .collect(Collectors.toList());
		

The Benefits of using Streams

  • Declarative – More concise and readable
  • Flexible
  • Parallelizable