Before reading this, I would recommend trying and learning what are imperative and declarative styles of programming.
Ref - Imperative vs Declarative
One of the powerful features of 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.
Streams API is used to process collections of objects.
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.
Streams don’t change the original data structure, they only provide results as per the pipelined methods.
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.
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.
Any stream might have an unlimited amount of data flowing through it. Data received from a stream is processed individually as it arrives.
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.
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.
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.
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.
From Collections.
From arrays.
From an arbitrary number of objects.
Infinite and finite streams.
Benefits
Following are the advantages of using Streams API.
We have already seen the difference between imperative and declarative approaches. With Streams, we move from imperative to declarative programming.
Streams can only be applied to Collections such as
List
,Map
,Set
,Arrays
, etc.Streams encourage less mutability
They can operate on their contents in parallel using the
parallelStream()
.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 +
'}';
}
}
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)
);
}
}
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);
}
}
Output:
J.R.R. Tolkien
Antoine de Saint-Exupery
Cao Xueqin
In the above example, we have a list of Book objects.
We applied
.stream(...)
to convert the list ofBook
objects into a stream of objects.We applied two intermediate operations
.filter(...)
that takes a Predicate and.map(...)
to get the filtered stream of objects.We terminated it with
.forEach(...)
to print all the objects.
Disadvantages
If streams are not handled properly, they have a huge impact on performance.
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.