In this post, we’ll learn Java’s Stream API by working with a sample data source using the Stream API. By the end of this article, you’ll have a basic understanding of the Stream API in Java and what kind of problem it solves.
Let’s start with the question “What is a stream?“.
Stream is basically just a wrapper for a data source that allows you to work with the data declaratively. By using stream, we can process data much faster than when working with it imperatively. Stream makes working with collection much more fun and easier.
First, let’s take a look at the data source that we’ll be using in our examples. Then we’ll compare how to work with the data source using both imperative and declarative methods so that you can see the advantage of using the stream API. After that, we’ll take a look at some common operations using stream by answering some questions about the data source, and letting the stream API answer it for us.
Note: I assume that you know the basic working knowledge of Lambda Expressions, Method Reference, and Optional
Introducing Our Data Source
First, let’s look at the data source we’ll be working with throughout the article. We’ll be working with a list of
Let’s first look at how
Cat is implemented:
Cat is a very simple object with 3 fields: name, age, and gender. We’ve also added getters and setters for each field and a
toString() method that’s going to be useful for displaying the result of our operations later.
Next, let’s look at the
Main class. This is where we’ll be working throughout the article.
For now, it only contains a list of
Working With Data Imperatively
Now, what if I want to create a new list that only contains female cats?
The standard way of doing this, the imperative way, is like this:
We just did all these steps:
- Create a new empty list.
- Loop through the
- In the loop, we check if the cat’s gender is female (F).
- If it is, we add the cat to the new list we created.
This is what it looks like if we print
Working With Data Declaratively
And now we’ll demonstrate how to work with the data declaratively using the stream API. Working declaratively means that we simply ask the program what we want without the need of specifying each step in detail just like what we did when working imperatively.
This is how we can do the same thing as we did above with the stream API.
So we start off by calling the
.stream() method on the collection (
cats), which returns a
Stream with the collection as its source. Now we can start working using the stream API. We simply have to ask what we want from the collection.
In this case, we want to filter out the list. We want a new list that only contains female cats. We do it by chaining
.filter() takes a
Predicate, which is basically just a functional interface that returns a boolean. In our case, we need to pass in
cat -> "F".equals(cat.getGender)).
The last step is to convert the result of
.filter() into a
.filter() returns a Stream. This is by design, and it’s really useful. We can chain the result of our
.filter() with other methods on the stream API which you will see later.
Now to convert it into a
List we have to call the method
.collect() take a
Collector as parameter. Since we want our end result to be a
Collector we need to pass in is
Finally, we get the same result if we print
Do you see the advantage of the declarative approach now? What used to be several lines of code in the imperative mode is now only a line, and the code is much more readable.
The advantage might not be too obvious now as the example was really simple, but it’ll become clear how useful it is when we try to do multiple operations by chaining stream methods later in this article.
Common Operation Using Stream API
Now, let’s take a look at some common operations using the stream API.
.forEach() simply loop over each element in the stream and apply the function you provide to each element.
For example, the code below prints the name of each cat.
.forEach() doesn’t return a Stream. So you won’t be able to chain another method after calling it. This kind of method is also called a terminal method.
.map() does the same thing
forEach() does. That is, applying a function to each element in a Stream. The difference is that
.map() returns a new Stream, which can be another data type.
Let’s look at an example of using
Say, We want to change the gender format of our cat from “M” and “F” to “Male” and “Female” respectively. We can use
.map() to solve this problem.
And if we print the
newCats, we get what we want.
We already looked at how
.filter() works before. It returns a new Stream that contains the element from the original Stream that fulfills a condition.
Let’s look at another example. Say that we want a new List of cats that are younger than 5.
And this is the result of printing
We can also sort the list using the
.sorted() method. The method takes in a
Comparator as a parameter.
For example, if we want to sort the cats by their age.
And this is the result:
You can also reverse it by adding the
.reversed() method like this:
And this is the result:
min and max
.max() are used to get the minimum and maximum values in a stream. To use the methods, we have to pass in a Comparator.
Note that the
.max() methods return an Optional because there might not be a result. So you’ve got to handle that.
Say that we want to find the youngest and the oldest cat from our list. We can that using
.max() like this:
And this is the result:
allMatch, anyMatch, and noneMatch
These operations take a Predicate as a parameter just like
.filter() and return a boolean.
.allMatch() returns true only if all elements in the stream match the Predicate.
.anyMatch() returns true if any one of the elements in the stream matches the Predicate.
.noneMatch() returns true if none of the elements match the Predicate.
collect is a terminal operation that we’ve been using several times in the examples above. It’s the most common way to end a stream and package the elements in the stream into a collection.
Of course, we’re not limited to collect into a list like what we did in the examples above. We can collect into any collection we want: map, set, etc.
Let’s look at an example of collecting into a map.
Say that I want to group my cats based on their gender. We can do it like this:
Now, if we print the map like this:
We’ll get this in the console:
Chaining Stream Methods
You can also chain multiple stream methods together. Which we have been doing all along in the examples above. But let’s look at it again using another example.
Say that I want to create a new List that only contains the name of male cats that are older than 4. We can do it like this:
That’s it for this article. I hope you’ve got a better understanding of the stream API now. I also hope that you can see how useful and powerful the stream API is, how it allows us to simply ask questions about our collection instead of implementing each step individually using the imperative approach.