It’s been a while since JDK 8 was released, bringing plenty of new features to the Java language; among them the most expected feature was ,with no room for doubt, the introduction of Lambda expressions. This release opened the door to a brand new Java functional style.
The release of Lambdas supposed one of the biggest quality jumps in the Java language in its history; mainly due to the fact that it opened a wide new range of possibilities to the language. Without them it’d be impossible to have the more flexible, expressive and simpler language that we can enjoy nowadays.
However, despite of having been available for a few years, I still see many developers struggle with them. That’s why I wanted to give a thorough explanation about them and more specifically about the biggest benefits in using them in our day-to-day as developers.
Let’s start then with a brief introduction to clarify some concepts!
What is functional programming?
Functional programming is a programming paradigm which has its roots in lambda calculus that uses a declarative approach; instead of the most commonly known imperative paradigm.
What this means is that we define what has to be done, instead of defining how it has to be done and in what order.
We make use of functions in order to declare what the application is intended to do.
Let’s see a couple of examples to understand what would be an imperative programming approach and a declarative programming approach using functions.
We’re going to implement a “greaterThan” functionality that will return the elements of an array that are greater than a given number. Let’s see how it would look like following an imperative approach first:
In this method we specify the steps that have to be done in order to get the numbers greater than “threshold“; specifying what has to be done and in what order.
If we use this method to get the numbers greater than 10 in our array:
The result would be: [23, 13, 26, 90]
That’s correct! There’s nothing wrong with our imperative implementation, it’s just that it’s verbose and for certain operations we could duplicate a lot of code throughout our careers. So let’s do it now following a functional approach in Java to see what the difference is:
In this example we make use of Java Stream to process the elements in the array, then we use a Predicate to tell the stream how to filter the elements.
A Predicate is basically a function which accepts an argument and returns a boolean based on the criteria specified on the predicate.
This is one of the many functions available since JDK 8 in package java.util.function in Java; we’ll talk about some of them later.
The result in this case is also [23, 13, 26, 90], same result but just a different way of getting it.
What are the differences following this approach then?
- We specify what has to be done, but not how
- Less verbose
- Reduces duplication of code
- Easier to read and understand
- Inherently immutable
This is very important for concurrent operations
- Impossible to change state using Lambdas
Also very important for concurrent operations, as we explained in the article “A new concurrency model in Java“
- It’s no longer possible to modify objects passed by reference as arguments (Unbelievably there are still some people that do this)
- Lazy evaluation
We can declare what has to be done, but the code in our functions doesn’t get evaluated until a stream gets materialised (to an array in our example) or a function is called.
Ok, so after this brief introduction about functional programming, let’s talk about its main component: functions.
First of all, there are some concepts with regards to functions that are important to understand in functional programming:
- Pure functions
A pure function is a function that: always returns the same output based on a given input; and also doesn’t have any side effects.
- Impure functions
On the contrary an impure function is a function whose output will not always be the same for a given input (mainly due to having some reliance on a shared state) or/and it does have side effects.
- Higher-order functions
A higher-order function is basically a function which accepts one or more functions as an argument or/and returns a function.
An example of impure function would be for example the following:
This function is impure because it relies on an external variable, therefore its output won’t be always the same for a given input! For example, if the external variable’s value is changed to 8, the result would be 16 instead of 18; we haven’t modified the input of the function (2) but we get a different result. That’s clear, right?
So what about a pure function? We will rewrite now the same function to make it pure, the result would be:
This new function accepts one more argument and it does not depend on any external variable; therefore the output of this function will always be the same for a given input! In our example the input is (9, 2) and as long as the input is the same we can guarantee that the result will always be 18; that, in conjunction with having no side-effects, is precisely what a pure function is.
So what’s really important about pure functions? Remember that we talked about non-sharing state and immutability in my recent article? Pure functions give us built-in support for that!
To make it easy to visualise, we have to think about pure functions as pipes. Yeah! Pipes. Pipes where our data travels through, in a complete isolated and safe way, where no other thread can touch them.
This is a very important concept to write concurrent programs in a much safer and simpler way!
Now that we have an idea of what functional programming is and its core concepts, let’s talk about how Java integrated these concepts and what did it bring to us since JDK 8.
Functional programming in Java
There are three main interfaces that you need to know in Java to be able to write functional code. These are:
A supplier is a function that accepts no arguments and returns a value.
A consumer is a function which accepts an argument but doesn’t return any value; as its name specifies, it just consumes something.
And finally a Java Function is a function that both accepts a value and also returns a value.
There are multiple variations of these tree concepts, but knowing these three interfaces you’ll be able to understand and do almost anything.
You can find a a list with the available interfaces in package java.util.function here.
Let’s take a look now at how these interfaces are used in Java.
Lambda expressions and anonymous classes
We can express any of these interfaces using Lambda expressions. Lambda expressions can be used to define functions without the need of existing as part of any class; the function itself is treated as an object and can be stored and passed as an argument in any way we see fit.
Lambda expressions basically have the following structure:
(argument1, argument2) -> expression
Before we proceed with some examples, it’s also important to talk about anonymous classes. An anonymous class is basically a way to create an instance of a given interface without the need of declaring an implementation of that interface; therefore we’re inlining the definition and instantiation of a class assigning no name to it, that’s where its name comes from.
Knowing that, we could then instantiate for example a Supplier, using either an anonymous class or a Lambda expression. Let’s see how it’d look like using an anonymous class:
This is perfectly valid, however is quite verbose; here is where Lambda expressions can help us. Let’s see how we can achieve exactly the same using a Lambda expression:
As you can see, using Lambda expressions we can express functions in a much more concise way, reducing boilerplate code. That’s much better, right?
Let’s see how all these interfaces could be expressed using Lambdas:
If you take a look at the last line, you’ll notice the use of “::” operator. This is the way to pass a method reference in Java; we can assign a method reference to a function when the only thing this function does is calling another method.
Apart from these existing interfaces, any single method interface in Java will be considered a functional interface. We could annotate them with @FunctionalInterface annotation, although this annotation is purely informative; it’s not going to have any effect on our interface at all.
The Java compiler will be capable of inferring the type of any functional interface as long as it has the same number of arguments and same result; therefore, we can create our own interfaces and apply them to any of the existing Java methods.
Let’s see a very simple example to prove that; we’re going to modify our “greater than ten” example to use a new interface created by us.
This is our newly created “GreaterThan” interface:
Because this function accepts an int and returns a boolean, we can replace the predicate we previously used in our example and Java compiler will infer the type. We could extract our previous code to a “elementsGreaterThan” method, which will accept a GreaterThan interface:
The method filter in our stream expects a Predicate, but because they have the same arguments and return type, the compiler infers the type without problem.
We will then call this method passing it the function we want and the result will be exactly the same:
I hope this silly example had helped you clarify what a functional interface is in Java and how could you take advantage of them!
Let’s now take a look at a very important part of JDK 8 release, Java streams!
One of the main features introduced in JDK 8 was Java Stream interface, which allow us to define a pipeline of operations to process a sequence of elements.
One of the important aspects of Java streams is that they are lazily evaluated; that means that our operations won’t be executed until we call a terminal operation. A terminal operation will be any operation intended to get a result, such as: forEach, collect, sum, etc
Another aspect to keep in mind is that a stream can only be consumed once; if we want to apply the same pipeline of operations again we’d have to regenerate the stream.
Let’s see an example:
Here we have defined a stream which transforms each String element to an Integer. At this point we have just declared the steps to be done, but they won’t be executed until we call a terminal operation.
Let’s do that now by adding all the resulting Integer elements provided by our stream:
Our stream won’t start processing the elements in the array until we call the terminal operation sum, which belong to IntStream interface.
As you can see, Java streams allow us to declare all the required steps for processing our data following a declarative paradigm. The result is a concise, expressive and elegant way of defining a pipeline of operations. That’s brilliant, isn’t it?
Think about any kind of complex pipeline that you could ever imagine, and think about how would you do that in JDK 7 without streams or functions; it’s almost guaranteed that your code would be quite verbose, repetitive and difficult to read. With Java streams we can easily write a concise series of steps to be applied to a sequence of elements.
Most popular Java stream methods
Let’s take a brief look at the most commonly used methods in Java streams:
we can filter a collection by specifying just a Predicate. We’ll receive a collection including only the elements that fulfil that predicate.
we can use map to transform every element of our collection to any other form we may need.
flatMap method is used for those cases where we have a stream of collections and we want to merge them all into one single collection.
this is one of the most difficult to understand, but also one of the most powerful and flexible in some cases. It’s used to combine all the elements into a single element.
So this has been an overview of the new features supported with the introduction of functional programming into Java in JDK 8. Let’s take a look now at what other features have benefited from its adoption.
Additional features supported
Now that we’ve gone through all the new functional features introduced in JDK 8, it’s time to take a look about what functions give support to in recent versions of Java.
We talked about CompletableFuture in two recents posts (“A new concurrency model in java” and “Combining multiple API calls with CompletableFuture“), and it uses functions extensively. It wouldn’t be possible to define callbacks and chain completable futures in the same concise and fluent way without them. That’s why functions are becoming so important in Java!
We also talked about Java Optional in the article “Please stop the Java Optional mess! and it also uses functions extensively to define concisely different behaviour depending on the state of the optional.
Functions allow us defining in one line what otherwise would have taken several lines and checking state with if conditions.
Collection.forEach and other collection improvements
Thanks to the introduction of functions we can now easily apply an operation to each element in a collection in a very neat and concise way using forEach method present in each Java collection.
collection.forEach(element -> System.out.println(element));
Other methods using functions could be Iterator.forEachRemaining or Collection.removeIf.
There are plenty of examples of how functions has enhanced the Java language recently, you’ll be able to find them yourself and experience with them to understand how important has this been for Java.
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.
If you are interested in learning more deeply about any Java topics, we recommend the following books:
The introduction of functions and streams in Java has been the biggest improvement by far in Java language in the last decade.
I think Java is now on the right path, becoming a better language for all and slowly removing some of the unnecessary attachments that were tying our hands, with a significance impact in our productivity.
Definitely the introduction of functions has open a new way of designing fluent interfaces in Java, which allows us to write code fluently in the same way as we could think in any common language as English. This has been a major improvement for our productivity and the readability of our code.
So that’s it from me! I had the intention to show some examples and tricks with streams, but I think this post is becoming a bit long to read. I’ll leave that for a next post, so please subscribe/follow to be notified when a new article gets published!
I really hope you’ve en joyed this reading and I’m looking forward to seeing you again!
Thanks you very much for reading!
You must log in to post a comment.