In traditional Imperative Programming, we used to write the code line by line to give the computer instructions about what we want to do to achieve a result.
For example, iterating through a collection of integers using 'for loop' to calculate sum is an imperative style. We created local variables and maintained the loop ourselves rather than focus on what we want to achieve. Mostly repeating a similar type of logic regarding looping and calculating every time.
What is a Java Stream?
The idea of Java streams is to enable functional/declarative programming on streams of elements.
A stream is a sequence of elements (you can think of these elements as data) that can be processed with operations. A stream is an abstraction; it's NOT a data structure. Even if NOT a collection for store elements. Streams are not data structures to organize data like a List or an array; instead, they are a way to process data that you can think of as flowing through the stream, much like water flows through a stream in the real world.
The most crucial difference between a stream and a data structure is that a stream doesn't hold the data. For example, you cannot point to a location in the stream where a particular element exists. You can specify the functions that operate on that object or data. A stream concept is an abstraction of an Immutable collection of functions applied in a similar order to data.
A simple stream that can be created from an array:
A stream life cycle or stream pipeline can be divided into three types of operation:
- A source that describes where the data comes from: A source might be a collection, an array or a generator function, etc.
- Zero or more intermediate operations operate on the stream and produce another, perhaps modified, stream—for example, filtering, sorting, element transformation (mapping), etc.
- A terminal operation which ends the stream and typically produces a result or some output, such as count, sum, or a new collection
Why use Streams?
Before we look into Java Stream API features, let us see why it was required. You might be wondering why streams were added to Java while we could accomplish the same thing using iteration over a collection.
The main reason to use streams is when you start doing multiple intermediate operations. The real power of streams comes from the "Intermediate Operations" you can perform between the source and the end of the stream. For instance, you can filter for even numbers (intermediate operation one), multiply each of those even numbers by 2 (intermediate operation two), and then display the results in the console (terminal operation).
Let's consider the below example :
Before Stream API in traditional imperative Java programming style, this would have required multiple lines if we wanted to fetch the employee names only starting with 'B' or 'C'.
However, when we use multiple intermediate operations, we start seeing the benefits of streams :
Look again at the code :
What do you think about what will happen if we write the following :
That is, we have written the code pipeline without the for Each() terminal operation.
Has anything actually happened yet? Is any data flowing through the stream?
The answer is NO. Nothing has happened. That's because no terminal operation has been executed yet. We have got everything set up but nothing to kick-start the data processing.
Not until the for Each() is executed does anything happen. As soon as the terminal operation for Each() is executed, the data starts flowing through the source and then into the operations.
This laziness makes the stream more efficient because the JDK can perform optimizations to efficiently combine the operations, operate on the data in a single pass, and reduce the data's operations whenever possible.
Map-Filter-Reduce with Streams
Let's take a look at the example. We'll define a List of Integers, take each Integer's square, test to see whether the square is greater than 20 and if it is, we will add 1 to a count, result. Before we do, we will look at how we used to do this before Java 8.
Here's the Java 7 way :
We are merely creating a List and using it for loop to iterate over the list, compute the square, increment the result if the square is greater than 20. Then print the result's value once the loop is finished.
Here is the Java Stream way :
The output is the same as above (Result (stream): 2).
Here, we are streaming the List of Integers, first mapping the number from the stream to its square, then filtering the squares to keep only those squares greater than 20, and then reducing using the count() operation to count the squares > 20.
Which code is better? We are very familiar with the for-loop iteration. The stream pipeline with a map-filter-reduce sequence is new to us. They both accomplish the same thing. Using stream is more efficient than using traditional for loop way.