Streams are, in my humble opinion, one of the best new features that were introduced in Java 8. Why? Because it lets us process data more quickly and efficiently than ever before and in a declarative way.
The best way to explain this is with examples.
Before we get started, it’s probably going to be best to get acquainted with Lambda Expressions, if you’re not already, as we’ll be using these in our examples.
For these we’re going to use a list of simple User objects that contain a name (String), date of birth (LocalDate – that looks new, we’ll touch on that another time) & active (boolean) status. We’ll leave the class file definition out for brevity but you can assume that all the normal get & set methods are present & that we have a constructor that sets all fields.
This is our list of users that we’ll be working with:
Let’s assume that we wanted to filter out all inactive users and get all names that begin with “G”. This is how we may have done it prior to Java 8:
Now let’s look how we can do this using Streams:
This looks a lot simpler & easy to read, doesn’t it? Let’s go through what’s happening.
First, we’re getting a Stream from our list of users. The “stream” method is conveniently available for all classes that implement the “Collection” interface. There are other ways to obtain streams as well, such as from arrays, generator functions or I/O channels, which we won’t look at here.
The next couple of parts add filter operations that should be performed upon the elements in the stream. The “filter” method accepts a “Predicate” parameter, hence the Lambda expressions, which are used to determine whether to accept or reject the element.
We then apply a mapping (transformation) operation, which accepts a “Function” parameter, that returns the name of the user.
Lastly, we apply a reduction by collecting all the elements left over into a list. “Collections.toList()” is a convenience method that allows this to be done without us needing to specify exactly how to create the list, although there are ways to be more specialised.
What we’ve done here is to construct a stream pipeline by specifying the different operations we want to carry out. We have a source, intermediate operations (filter & map) and a terminal operation (collect).
An important point to raise here is that intermediate operations, of which you can have zero or more, create new streams that contain the results of applying the operation to the initial stream.
Another is that intermediate operations are lazy in the sense that they will never be executed until the terminal operation of the stream pipeline is executed. A key benefit of this is that when it comes to processing the pipeline operations can be performed more efficiently, such as when needing to find just the first X elements.
The terminal operation is the one the starts processing of the pipeline & produces a result or a side-effect. Once executed, the stream is closed & cannot be used again.
So, now that we’ve had the explanation of what is happening let’s have a look at more examples of what we can do.
What if we wanted to sort the list of names we had before? Easy – we can use the “sorted” intermediate operation. This example uses the natural order of Strings but you can supply your own Comparator should you want/ need to:
How can we get just the first 2 active users that we find?
Let’s have a look at some examples that produce a result other than a list now.
Could we group our users by their active state? Sure:
“Collectors.groupingBy” creates a map where the key to be used comes from applying the passed “Function” on the stream elements & the value is the list of elements to group under that key. There are other “groupingBy” methods that you can if you want more control over the type of map created or the collection being used to contain the values.
Can we get the total number of active users?
What if we wanted to get the combined age of all our users? For this let’s assume that there is a method, getAge, that we can use to get the age of a user:
Here we have a couple of options.
In the first instant, we’ve supplied a reduction to use where we have an initial value to use & a way to combine the results, which in this case is a straight forward addition.
In the second we’ve use a mapping function which returns an IntStream. This is a specialist stream that deals with the “int” primitive type in a more efficient manner as well as providing some handy convenience methods. “sum” is one of these which basically does the same as the reduction we used first.
It’s also worth noting that LongStream & DoubleStream are available as well for when you want to work with their respective primitive types.
For our final example, what could we do if we wanted to perform an action for each user such as printing the result of calling its “toString” method to the standard output?
Simple, we just pass a “Consumer” parameter to the “forEach” method & it’s invoked for each element in the stream.
So, there we are. We’ve seen how streams can greatly simplify the amount of work needed to process data, even more so when you’re able to use method references instead of lambda expressions. I hope that you agree that they are a great addition to the JDK.