In my last article, we talked mostly about how Java Streams work and we also had an introduction to Functional Programming in the article “A new Java functional style“; now it’s time to take these learnings and see how we can use Streams and take all their benefits.
We’ll go through a set of examples with different complexities to try to show how Java Streams and functional programming can help us solve complex problems that we could face in our day-to-day work as developers.
Let’s see how can we use Java Streams then!
How to use Streams
As you could expect, to start using Streams the first thing we need to know is how to create a stream! Let’s see the different ways of creating a Java Stream.
Creating a Stream
There are multiple ways of creating a Stream in Java, let’s go quickly through some of them:
- From an array
A stream can be created from an array by using the factory methods Arrays.stream or Stream.of
- Using a generator function
There are different ways to create a generator function in Java. For example by using Stream.iterate we can create an infinite stream of BigDecimal adding ten on each iteration:
Another example of a generator function could be Stream.generate method.
- From a Spliterator
Java also provides a StreamSupport.stream method to create a stream from a Spliterator class.
- Creating a range
We can also use the methods range and rangeClosed, which are present in all the primitive-based streams like: IntStream, LongStream or DoubleStream.
An example would be:
That will create a Stream from 0 to 100, both inclusive, printing these numbers on our console.
We know now different ways of creating a new Stream but actually they can be created from any data source we could imagine if for example, we create our own implementation of Spliterator as it was explained in the last article.
I think we’re ready to start playing around with them. Let’s take a look at some of their operations!
Filtering elements since Java 8 has become very easy; instead of having to use an indexed-based loop to manually get the elements we’re interested in, since JDK 8 we can just use the filter method and specify a Predicate that our elements should fulfil in order to be picked and be part of our Stream.
For example, let’s get all names that start with “S” from a list of strings:
It’s worth mentioning that collect method is a method that mutates an existing value. Why do we say that?
That’s because the way it works internally is that it instantiates an empty ArrayList initially and then it keeps adding elements to this ArrayList; therefore we can say that it mutates this collection.
Another common operation that we’ll use very frequently is transforming elements into a different form. In the past we used to do transformations by directly mutating the object in question; as we saw in one of my recent articles “A new concurrency model in Java“, mutating objects should be avoided because it isn’t thread-safe and complex synchronisation is required if our code gets accessed concurrently; we should then be creating a completely new immutable object instead.
This is basically what we should be doing with Java Streams; if we keep our functions pure avoiding state mutation, we can guarantee that it’d safe and easy to work with parallel Streams.
Since JDK 8, if you know how to use Java Streams properly, you now know how to write concurrent code; as simple as that!
Let’s look at an example. Let’s say that we have a list of employees and we need to know the set of different ages among our employees for statistical purposes. How can we do that?
What we have to do is just create a stream from our collection of employees and map our employees’ ages into a new collection. This is how it’d look like:
As you can see, we’re passing Employee.getAge() as a method reference; as we explained in the last article, when a Lambda function just calls another method, we can simplify it by passing its method reference.
This is probably one of the most “mind-blowing” operations, specially for those developers not used yet to follow a functional approach.
Java provides a way to combine elements that is called reduce; using this operation we can combine every consecutive pair of elements and apply an operation over them.
To show an interesting example where we will see some interesting facts about Streams; we’re going to implement the factorial of a number using reduce method.
If you remember what a factorial of a number is, it’s basically the product of each number in a series where each number is less or equal than N.
4! = 4 x 3 x 2 x 1 = 24
We’re going to implement this using reduce method, which accepts a BinaryOperator. BinaryOperator is a type of BiFunction, which is basically a Function that accepts two arguments. So it’s format is something like:
(arg1, arg2) -> expression
Let’s see how our implementation would look like using reduce then:
Basically what we’re doing is creating an IntStream from 1 to N, we convert it to use boxed type instead of primitives to be able to sort it and then we sort it in reverse order. We’ll get the values in this order then (4, 3, 2, 1) and to calculate the factorial we use reduce terminal operation combining the elements. What our stream will be doing is the following:
(4, 3) -> 4 * 3 = 12 (12, 2) -> 12 * 2 = 24 (24, 1) -> 24 * 1 = 24
You can see that because Java Streams are lazily evaluated, our Stream is not iterating through the elements in order to reverse it; it knows how to apply the operations correctly to behave as expected when a terminal operation is invoked.
Also notice that we don’t really need to go in reverse order to calculate the factorial, but it was a better example to show it in the way that we naturally calculate factorial.
public static <T,K> Collector<T,?,Map<K,List<T>>> groupingBy(Function<? super T,? extends K> classifier)
I know, its signature looks complicated, but it’s simpler than what it looks like! Just forget about the clutter introduced by the use of generics and we’d have a Function as an argument that will define how to classify our data.
We’ve already learned what a Function is in our last article, we will just have an employee as the input and we will produce whatever is the element we want to group the collection by.
So let’s go through a simple example then. First of all we’re going to create an AgeRange Java enum to classify our employees by age.
As you can see, our enum provides a static method to provide the appropriate AgeRange for a given age.
The way we use groupingBy is by providing our collector to a collect terminal operation; this is how it’d look like:
It’s pretty simple, right? We specify how we want to classify our elements by providing a Function, that’s it!
What about the other groupingBy methods available? For example:
public static <T,K,A,D> Collector<T,?,Map<K,D>> groupingBy(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream)
This method accepts a second argument which is a Collector; we’ll see now how powerful this would be for grouping elements!
We are going to transform our previous example to show the average salary for each age range. We just have to provide a way to collect our already grouped data and Java provides plenty of collectors that are available to us. Remember that they are in Collectors class!
Keeping in mind that our salary is a double, if we take a look at Collectors class we can find the averagingDouble method. Knowing that, calculating the average salary for each age range is as simple as this:
That’s brilliant, isn’t it? We can do any of these operations very easily with Java Streams, we just need to provide the right functions and understand this new concept of functional programming. Once we have a good command of that, our possibilities are almost limitless!
This way of programming is clearly less error-prone and much more readable than the old way of doing things.
Sorting elements in Java is quite straightforward using functional programming. Java provides two sorted methods in our Java Streams; one of them accepts no arguments and the other accepts a Comparator as an argument.
int compare(T o1, T o2);
What that means is that we have to provide a function that accepts two arguments (the two elements to compare) and returns an int as the result of comparing the elements.
This looks quite easy, but what makes things even easier is that Java provides methods to allow us to do comparisons with no effort. Let’s see how can we sort our employees collection from previous examples by the employee name for example:
You’ll notice that we’re using a static method on Comparator class. We have to remember that Comparator is a functional interface, so it can only have one method, but that doesn’t stop it from having many static methods that help us create comparators easily.
Comparator.comparing is one of them; this method accepts a function that accepts one of the elements we’re trying to sort and it’ll return the key that we’ll use to sort these elements.
In our example we use a method reference, but this would be exactly the same as doing:
.sorted(Comparator.comparing(employee -> employee.getName()))
So that’s the way to sort elements with Java Streams, it’s quite straightforward once you understand how functions work in Java.
Let’s take a look now at more complex examples where we’re going to use all we’ve seen so far!
Find highest price for a stock in a file
We’re going to build an example where we have a file with a list of stocks and their values in different currencies. The format in the file is as shown below:
AAPL 353.63 USD
What we’ve been asked to do is to convert all those prices to a given currency to be able to compare them and then get the highest share price among all of them.
Let’s see how can we do this; first of all, we’ll have to transform our share price to the selected currency, so in order to do that we’ll use map.
Once we have the values in the selected currency, we’ll have to keep track in some way of what’s the maximum price so far.
How can we do that? There are different ways of doing this following an imperative paradigm, but what’s an efficient way of doing this?
- Using a standard Java loop
It’ll work, but we’ll have to mutate the state of a variable to keep track of the highest price. What’s the problem with that? Well, what if we wanted to run our implementation in parallel? We wouldn’t be able to do it, right?
- Using recursion
Another possible approach that could work would be using recursion. It’d work but what would be the problem with this solution? The problem is how Java stack works internally.
We have to remember that when we use a recursive method every time we call a recursive method, our method invocation gets stored in the stack.
The problem is that the size of the stack is limited, so if our file is big enough, we’ll be soon raising a StackOverflowError!
So what’s the right approach then? A safe approach that allows us running our solution in parallel if we wanted to.
- Using Java Stream’s reduce method
By using reduce method, we’ll be comparing each pair of elements and returning the highest of them. The good thing about this approach is that it can be parallelised as much as we want, the only important thing is that when we merge all the elements we get the highest. Let’s see how it’d look like:
Basically what we’re doing is creating a stream from each line in our file, then we convert the value from its original currency to pounds by using a third-party currency converter. Once we have that then we apply a reduction getting the max value between each compared pair of elements.
This implementation will allow us to use as much parallelisation as our CPU allows with no problem. I hope that makes sense!
So that’s it from me this time! I hope you’ve enjoyed our journey together through Java Streams!
If you need to improve your understanding of Java Streams and functional programming in Java, I’d recommend that you read “Functional Programming in Java: Harnessing the Power Of Java 8 Lambda Expressions”; you can buy it on Amazon in the following link.
We’ve seen how now we can follow a more functional and declarative approach to solve problems in Java. This is a much more expressive and fluent way of programming that I personally enjoy much more now; also this is less error-prone as the code behind Java Streams has already been throughly tested, so I don’t have to worry about implementation details, just focusing on what has to be done! I think this is a huge improvement for us as Java developers.
Functional programming help us solve problems in a much simpler and safer way than before, opening new possibilities to the way we write our code.
I hope you’ve enjoyed this reading and that you’re looking forward to reading more about Streams and functional programming. If you liked this article, please subscribe/follow to be notified when a new article is published!
I wish I could’ve showed a few more examples, but for the sake of brevity it’s difficult to condense everything in one article, maybe in one of my future articles!
Thank you very much for reading!