Have you ever thought about Functional Interfaces and how they work in Java? Let’s see them now!
First of all, do you know what a Functional Interface is?
Functional Interfaces are interfaces which have a method to be implemented, in other words, an abstract method. It means that every interface created that respects this premise, automatically becoming a functional interface.
The compiler recognizes those interfaces and enables them to be available for the developers to work, for example, with lambda expressions.
Today, we are going to talk about the primary Functional Interfaces presented on JDK, which are:
Checking its class, we can see this below:
What can we conclude with that?
The letter T means it's generic (generic means that it can be of any type), and in this case means that the operation get(), when executed, is going to return something for us and it can be from any type.
On the other hand, we do not need to pass an argument. In short, we call it and receive something - like a supplier (did you understand the name now?).
Let’s see an example:
Here we are calling the method generate() from the Stream API, which needs a Supplier to be executed.
So, we are passing nothing to the method using the empty brackets ‘()’, then using lambda ‘->’ and finally executing the method - new Random().nextInt() - that is going to return something for us (in this case, a random number).
Here, we just added the limit and forEach to show you the result on the console.
So, if we run the code, we will get:
That’s how the Supplier works - we don’t need to provide anything and we received a response.
Let’s check its class:
It’s a simple interface, the opposite of Supplier. It receives a generic variable, does something with it and then, returns nothing.
Example:
We got a list of Integers and to print out these numbers on console, we can use the .forEach to help us with that.
It is receiving a variable - number - and doing something with it. In this case, it is printing on the console and returning nothing for the user.
Just like a consumer. It gets something, does something and that’s all.
BiConsumer follows the same rules but it receives two arguments.
In the example above, we are receiving two integer values and just printing them in the console and returning nothing.
Now, let’s take a look at the Predicate and BiPredicate classes:
The Predicate class has a method called test, which receives an argument, and it returns a boolean.
We can conclude that this method is used to validate hypotheses. Let’s see in the code:
For this example, we got a List of Integers which is composed with the numbers - 1, 2, 3, 4 and 5.
And again, we are going to use the Stream API. We are going to convert our list to a Stream through the .stream() - then we are going to use the .filter() - and it’s on this little guy where magic happens with the Predicate.
Our method filter needs a Predicate to be executed, and as we have seen before it will validate a hypothesis and return True or False for us.
In this method, we are getting the number and checking if it is divisible by 2 - which means it is an even number. If it is true, then we will execute the forEach, otherwise it will be ignored.
That’s how Predicate methods work: they test hypotheses and return to you if they are true or false.
If we run this code, we get this result:
It just printed the even numbers, as expected.
And regarding the BiPredicate, we got the same behaviour, the only difference is that we are going to receive two parameters to be checked instead of one. Check it below:
Example:
Here we got a BiPredicate receiving two parameters - word and size - and checking if they have the same value.
In the first test, we are going to receive True, and in the second one, we are going to receive False.
The functional interface Function is the most generic. It has the most basic definition of a function - it receives something and returns something.
Let’s take a look at the class:
Let’s get started with the Function. Here we can see that the method applied receives a generic variable - remember generic means it could be of any type - and it will return another generic variable.
One important thing here is that even if the generic words are different - T and R - it does not mean that the apply() method cannot receive and return the same type.
So, let’s go to the code - Once more, we are going to use the Stream API for this example.
Here we are going to use the method .map(), which needs a Function to be executed.
So, in this code:
We are getting the number, which is an Integer value, and returning a Double. So, we are passing an argument to the map and receiving a response.
*In this case, we are giving an Integer value and receiving as response a Double value.
But, for example, we could do this:
It is an Integer value as argument and its return is an Integer value as well. Function permits that. Regarding BiFunction, it has the same behaviour, except it receives two arguments as well as BiPredicate.
Let’s check it in the examples below:
In the first example (sumNumbers), we got a BiFunction that receives two arguments - Integer type - and returns an Integer type as well. We are doing a sum and when we use the apply() it results in the number 3 (1 + 2).
Next, we got another example, but, this time, it is returning a Double value. We got 2 Integer numbers and, after calling the Math.pow(), it returned a Double value. Once we run the apply() method, we will get the result: 4.0 (Double)
To sum this up - we give one or two arguments and receive something in exchange. The basic meaning of a function.
Let’s take a look at its class:
The UnaryOperator extends a Function - but on its constructor is defined the same type of arguments, did you notice that?
We have UnaryOperator <T> extending to Function <T, T>.
It is defining the type allowed for this interface, and as all these generics are the same letter (T), it means that we are just allowed to work with the same type of variables - our arguments and returns must be the same type to work in the UnaryOperator.
UnaryOperator has the same behaviour as Function, but it only works with the same types.
For example:
You can just define one type on its constructor, and it will be extended to the Function, and as we can see in the code above, we can just work with variables of the same type.
It’s the same thing as the others.
It extends to BiFunction. So, we are going to receive two arguments and return one - all with the same type.
Let’s check this out:
We use Stream API again.
Now, we are going to use the .reduce() method. It will receive two arguments and will return a value with the same type.
In our case, we got an Array of numbers - 1 to 5 - and we are going to sum all the values.
*As reduce returns an Optional value, we are going to use the .ifPresent() to print our result.
So, as result here, we will have: 15 (1 + 2 + 3 + 4 + 5)
*It is going to repeat until the whole array passes through it.
To wrap things up, let’s see an example of everything working together:
Just one last thing - the functional interfaces also accept Method Reference - the code could be like this:
That’s it! I hope it can help you to create better and cleaner code! As well as help you to understand how the functions work.