Hey Tea Lovers! In this post, we will be talking about an unsung hero of the Java world, the Stream API which was added to Java 8 and has changed the way we do programs in java and how! It added neatness as well as made the program more readable. It helps you do functional programming in Java. Without any further ado, let us read more about this jewel.
What is Stream API
The Stream API, included in Java 8, is utilized for processing collections of objects. It is the flow of Objets on which various methods get applied in the pipeline to have the result. Simply put, it flows through the given
Collection<Object> and applies the different methods on the
Object to aggregate them into the desired result without affecting the original
What Stream Provides
Stream doesn’t store anything instead, it operates on the given
Array or I/O (yes you can use it on I/O) Without changing the original data, it applies the methods to the data. Lazy evaluation helps to add multiple intermediate operations without actually utilizing them at the moment of building it. It gets evaluated only after we terminal operation. The Stream API also adds neatness to the code to make it more readable. And yes, Functional Programming is a key feature.
Prerequisites to Start Using Stream API
Stream allows you to do functional programming in Java. This means you should be comfortable with functional interfaces and lambda. Don’t worry, You just need the basics of it, which you can do with the help of this post, “Be More Functional With Java’s Functional Interfaces”. If you know these well, nobody will stop you from implementing the Stream API. The examples used in this post, you can find the code on GitHub here or the full project here. And yes, fill your cup of tea, start sipping and see more about this hero.
How to use Stream API
We can do this in multiple ways. Such magic tricks are,
- One of Stream’s static factory methods such as,
- Streaming the line of a file by
- Streams of random numbers can be obtained from
1st and 2nd are what we use most often. For our discussion, we will be sticking to the Collection’s stream method. For Example,
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); list.stream() // multiplying each number by 2 .map(n -> n * 2) //taking forward only even numbers .filter(n -> n % 2 == 0) //printing each even number .forEach(n -> System.out.println(n));
Stream Pipeline Operations
We can perform two operations on a Stream pipeline
- Intermediate Operation
- Terminal Operation
Intermediate Operation transforms or filters the object in the preceding pipeline. These methods are nothing but the functional interfaces we discussed in our previous post. You should get familiar with them to easily understand this operation without any trouble. Stream won’t execute intermediate operations unless a Terminal Operation gets called.
Terminal Operation is an indication for the Stream to start the pipeline. Once we call the terminal operation pipeline ends and it returns the desired result. After the terminal operation, we cant use intermediate operation since the pipeline has ended. It can only be called once at the end of the pipeline. However, you can again start the stream if the terminal operation returns a collection. Which I highly oppose doing.
You can pretty much everything inside a single stream pipeline only.
Now, let us see these operations one by one.
Stream API Intermediate Operations
In the Stream pipeline, we can have multiple intermediate operations that transform or filter the data which will be flowing towards the next pipe without affecting the actual data on the Collection. These operations return the Stream of the desired result instead of the actual result i.e
Stream<T>. With this, we can add more intermediate operations without breaking the Stream pipeline. We will be talking about the
filter in this post and others in another part.
filter are the ones you will be needing most of the time.
These methods are applied one by one to the elements in the pipeline and not all at once.
Stream API map()
Think of it as a transforming function, which transforms the given object to another. It accepts a
Function<T> as a parameter, which takes the element in the stream as input, performs some operation, we tell it to do, and returns the
Stream of the resulting object. Learn more about
If we look at our previous example, it is multiplying each number by 2. We can have another map which can add 2 to the multiplied number and then another one to convert it to another object. and then another one and so on, you get the idea.
list.stream() // 1. multiplying each number by 2 .map(n -> n * 2) // 2. Converting to string. .map(n -> "CodersTea.com-Post No : " + n) //3. Now you will be working on the //stream of String //4. we add another string to the previous .map(string -> string + ". Another String") //5. print the end result .forEach(System.out::println);
In the above example look how we transformed a list of
Integer to the
String. Lets breakdown it.
- Multiplying each number by 2. So 1 becomes 2, 4 becomes 8
- We have attached the number to a string resulting in a string object.
- The previous map made the
Stream<String>due to the return type of string.
- We again attached another string to perform some operation.
- Finally printing the results. Which looks like,
CodersTea.com-Post No : 2. Another String CodersTea.com-Post No : 4. Another String CodersTea.com-Post No : 6. Another String CodersTea.com-Post No : 8. Another String CodersTea.com-Post No : 10. Another String CodersTea.com-Post No : 12. Another String CodersTea.com-Post No : 14. Another String CodersTea.com-Post No : 16. Another String CodersTea.com-Post No : 18. Another String CodersTea.com-Post No : 20. Another String
Stream API filter()
The filter, as the name suggests, filter outs the elements before it reaches the next pipe. Simply put, if the element satisfies the condition it allows it to pass through otherwise it won’t allow for the next operation. It takes a
Predicate as a parameter. Know about Predicate here.
long count = list.stream() // 1. Need only even numbers .filter(n -> n % 2 == 0) // 2. Checking if the Number is // greater than 5 .filter(n -> n > 5) //3. multiplying the number by 5 // just to show how we can use multiple // Intermediate operation together .map(n -> n * 5) // 4. Any number less than 50 .filter(n -> n < 50) // Counting how much elements // survived teh pipeline .count(); System.out.println("Total " + count + " numbers survived the storm");
God, so many filters. let’s break this down, shall we?
- Take only even numbers ahead.
- Forward only those numbers which are greater than 5
- Multiply the number by 5. Again, just to show you can have multiple Intermediate operations in a single Stream pipeline.
- Allow numbers that are less than 50.
count(Terminal Operation) how many survived after this much filtering.
Total 2 numbers survived the storm
Stream API Terminal Operation
Till now we have seen the operations which perform something in the pipe and return the stream. In the end, we will be needing a final result, which is not a stream, don’t we? That’s where Terminal Operation comes. It does not only give the desired result but also starts the Stream. As I said, Stream is lazy, it won’t do anything, no matter how many intermediate operations we add to the pipeline, until and unless it sees its end, the terminal operation.
You cant use the same stream after it was closed by a terminal operation. Otherwise, it will throw IllegalStateException: stream has already been operated upon or closed. You have to start a new stream
The terminal operations we will be using for our examples are
count. Let us examine them and a few others.
Stream API forEach()
This iterates over each element passed by preceding intermediate operation. It takes input but hs void return value. It takes
Consumer as a parameter(more on Consumer). Use this if you want to do something without returning anything. We have used it for printing the numbers.
list.stream() // square of each number .map(n -> n * n) //prints each element .forEach(n -> System.out.print(" " + n));
As the name suggests, it is used for counting the elements reached to the end. Like in filter example, counting the number who satisfied all the filter’s conditions.
long evenAndLt5Count = list.stream() // even numbers less than 5s .filter(n -> n < 5 && n % 2 == 0) .count(); System.out.println(evenAndLt5Count);
I use this more often than any other terminal operation. It allows you to create the
Collection from the elements of the stream after processing them. These
Collection can be
Map. The Collection includes elements that passed through the last pipe.
List<Integer> processedList = list.stream() // multiplying by 5 .map(n -> n * 5) // numbers less than 30 .filter(n -> n < 30) //add +2 .map(n -> n + 2) // give the list of processed numbers .collect(Collectors.toList()); System.out.println("processed List is \n " + processedList);
In the pipeline we have multiplied the number by 5, then filtered numbers less than 30 and added 2. And after all the elements are done process, we collect them into the list.
processed List is [7, 12, 17, 22, 27]
The Last Sip
We have studied what is Stream API in java, how to obtain it, how to operate it, use it. This post is kind of like an introductory post for the Stream API. There are so many diamonds in the Stream mine, which we will be mining in the next post. I highly recommend you check out “Be More Functional With Java’s Functional Interfaces” for a better understanding of the Stream API operation. You can find the code on GitHub here or the full project here. See you in the next post.