A Guide To Functional Interfaces in Java10 min read
A functional interface is an interface that contains exactly one abstract method. However, it still can have an arbitrary number of default or static methods. Talk about a functional interface people often think about Java 8 because this concept originally emerged at that time along with a bunch of new features that were added. With the existence of these interfaces, we can perform the functional programming approach which makes the code much easier to read. A functional interface is also called as a Single Abstract Method interface or SAM interface.
When you search for pre-defined functional interfaces in the Java Docs, you might feel a little overwhelmed because there are too many of them. Some of them we can include here are Function<T,R>
, BiFunction<T,U,R>
, IntPredicate
, Supplier<T>
, BiConsumer<T, R>
, etc…For more information about these standard interfaces, check out the official docs here.
However, you should keep in mind that every interface with only one abstract method can be classified as a functional interface, not just these predefined interfaces. Before Java 8, there are a lot of legacy interfaces such as Runnable
or Callable
, etc.. are also examples of functional interfaces.
Familiar with the concept of functional interface is a crucial step before diving and using predefined functional interfaces or creating your own.
Table of Contents
Create your own functional interface
We can create our own functional interface, let’s demonstrate that by create an interface called FIDemo
with one explicit abstract method called greeting()
(methods on an interface are public abstract
by default):
@FunctionalInterface
interface FIDemo {
void greeting(String name);
default void greetingDefault() {
System.out.println("Hi, welcome to learntocodetogether.com! Happy coding!");
}
}
public class FIDemoImpl implements FIDemo {
@Override
public void greeting(String name) {
System.out.println("Hello " + name);
}
public static void main(String[] args) {
FIDemoImpl obj = new FIDemoImpl();
obj.greetingDefault();
obj.greeting("Ethan");
}
}
We created a class FIDemoImpl
that implements the functional interface FIDemo
. As stated earlier, besides a single abstract method, it’s legitimate to create other types of methods such as default
or static
in the functional interface, the default method in the FIDemo
provides a concrete implementation. The output when we run this code will be:
Hi, welcome to learntocodetogether.com! Happy coding!
Hello Ethan
Functional Interface and Lambda Expression
It would be a deficiency if we talk about functional interfaces but forget to mention lambda expression. Before Java 8, because we cannot directly create a new instance from an interface, the feasible solutions at that time were to a concrete class with a lot of unnecessary boilerplate to just encapsulate a single piece of data (as we did in the first example), or less verbose we can make the use of an anonymous class like this:
FIDemo anonymous = new FIDemo() {
@Override
public void greeting(String name) {
System.out.println("Hello " + name);
}
};
anonymous.greetingDefault();
anonymous.greeting("Ethan");
However, there is now a much shorter and concise way to implement and instantiate a functional interface with a lambda expression (this syntax is only used with functional interfaces):
FIDemo lambda = name -> System.out.println("Hello " + name);
lambda.greetingDefault();
lambda.greeting("Ethan");
There is an abstract method void greeting(String name)
in the FIDemo
interface, we have implemented this method with the help of lambda expression. The greeting()
method accepts one parameter, on the left-hand side of the arrow we need to include a parameter there but we don’t need to include its type, on the right side of the arrow, this is the implementation of the greeting()
method. Parameters and a returned value of a lambda expression correspond to parameters and a returned value of SAM in a functional interface. Here greeting()
return void
, hence this lambda expression also returns void
.
Notice that the type of the interface in the left, and the type of the lambda expression has the same semantic meaning. For a more detailed guide about lambda expression, check out this article.
By using the lambda expression, we actually implement this interface by overriding its single abstract method.
Besides lambda expression, a more elegant approach to work with functional interfaces is by using method references, but we won’t talk about it in scope of this article.
Functional Interface Annotation
As in the first example, we saw a syntax @FunctionalInterface, this is the special annotation defined in the Java Class Library (JCL) which signifies a functional interface, and if an interface with this annotation doesn’t satisfy the functional interface definition the code will compile with errors. Even though there is no obligation, including this annotation is a good practice to mark functional interfaces. Example of a non-functional interface:
@FunctionalInterface
interface FIDemo {
void greeting(String name);
void sayGoodbye(String name);
default void greetingDefault() {
System.out.println("Hi, welcome to learntocodetogether.com! Happy coding!");
}
}
If I add a new abstract method sayGoodbye()
here, even I override this method on the implementing class, the code still won’t compile due to the @FunctionalInterface annotation:
Error:(1, 1) java: Unexpected @FunctionalInterface annotation
FIDemo is not a functional interface
multiple non-overriding abstract methods found in interface FIDemo
Functional Interface in Java 8
By applying functional interfaces, your code can look much shorter and easier to read. When Java 8 came out, there were about 40 new function interfaces added in the java.util.function
package. They are classified into five groups:
- Predicates: accept one argument and return a boolean value.
- Functions: accept arguments and produce results.
- Suppliers: accept no argument and return a value.
- Consumers: accept arguments and return nothing.
- Operators: specialization of Function which produces a result of the same type as its operand.
Predicates
A predicate is a function that receives an argument and returns a boolean value. For example, we have a predicate to check whether a number is even:
IntPredicate isEven = n -> n % 2 == 0;
isEven.test(10); // returns true;
This IntPredicate
interface has one abstract method called boolean test(int value)
which takes an integer value. We use a lambda expression to override this method and right underneath we try out this method by passing the value of 10, which returns true
.
Another predicate interface I want to mention here is the Predicate<T>
interface, which accepts a type parameter T. It also has one abstract method test(Object obj)
which accepts an object and return a boolean value:
Predicate<String> lengthGreaterThan5 = str -> str.length() > 5
lengthGreaterThan5.test("Learntocodetogether.com"); // true;
lengthGreaterThan5.test("Hi"); // false;
We override the test()
method, then this predicate will check if we pass a string with a length greater than 5 or not. Below, we call the test()
function which we just override, pass some strings to this method, and then get result true
or false
.
There are some other predicate functional interfaces in Java: BiPredicate<T, U>
, LongPredicate
, DoublePredicate
.
Functions
The interface Function<T,U>
has one abstract method R apply(T t)
which takes an argument type T and produces the result of type R, which corresponds to the interface parameterized type arguments. Let’s implement this interface:
Function<String, Integer> strLength = str -> str.length();
strLength.apply("Learntocodetogether.com"); // 23
strLength.apply("wubba lubba dub dub"); // 19
Basically we override the apply()
method, we provide an argument with the type of String (type T), in the body of this method, we return an Integer (type R) which fit to the interface declaration Function<String, Integer>
.
Another interface BiFunction<T, U, R>
represents a functional that accepts 2 arguments and produce a result, whereas:
- T is the type of the first argument
- U is the type of the second argument
- R is the type of the result of the function.
The abstract R apply(T t, U u)
takes 2 arguments and return a result type R. Let’s implement a simple BiFunction<T, U, R>
interface which accepts 2 integers and return their product:
BiFunction<Integer, Integer, Integer> product = (x, y) -> x * y;
product.apply(10, 5); // 50
product.apply(2, 3); // 6
It is necessary to include the parentheses on the left side of the lambda expression when there are more than one parameter.
There are a lot of other interfaces as well such as IntFunction<R>
, LongFunction<R>
, DoubleFunction<R>
, ToLongFunction<T>
and so on.
Suppliers
Interfaces belong to this group don’t take any argument and produce a result. For example the Supplier<T>
interface has one abstract method T get()
which don’t accept anything and return a value type T. For example:
Supplier<String> str = () -> "Hello everyone"; // returns "Hello everone" type String
Supplier<Boolean> bool = () -> true; // returns true type boolean
Supplier<Double> doubleSupplier = () -> 12345.6D; // returns 12345.6 type double
We need to include the parentheses when there is no argument in the lambda expression syntax. There are other suppliers: LongSupplier, IntSupplier, etc..
Consumers
Interfaces belong to this category represent an operation that accepts one or two arguments and returns no result. Unlike most other functional interfaces, consumers come in handy when we don’t need to return anything and produce the side-effect. For example, the interface Consumer<T>
has an abstract method void accept(T t)
which accepts an argument type T and return nothing:
List<String> names = Arrays.asList("Ethan", "Nam", "Bob", "Alice");
Consumer<List<String>> consumer = (list -> {
for (int i = 0; i < list.size(); i++) {
list.set(i, "Your name is: " + list.get(i));
}
});
consumer.accept(names);
names.forEach(name -> System.out.println(name));
Let’s break down what’s happening here:
- First we create a list of names with 4 elements.
- Right below we create a consumer which takes parameterized type of
List<String>
, which is the type ofnames
variable which we have defined. - On the right hand side of this
consumer
, we implement theConsumer
interface by overriding theaccept()
method, here we pass in alist
with type ofList<String>
, iterating through each element of this list and modifying each element. - Then we call the
accept()
method on this consumer, passnames
with the type ofList<String>
to it. - At the bottom, we print each element of this modified list with the
forEach
method, interestingly, this method also accept a Consumer, each element on the list will override theaccept()
method, again it doesn’t return anything and just print out each element on the list.
The list now has been modified, here is the output:
Your name is: Ethan
Your name is: Nam
Your name is: Bob
Your name is: Alice
Here are some other consumers: IntConsumer, LongConsumer, DoubleConsumer, BiConsumer<T, U>, etc…
Operators
These Operators
functional interfaces present an operation that takes 1 or two operands and return the result has the same type as their operand. For example, the IntUnaryOperator
has an abstract method int applyAsInt(int num)
which accepts an integer and return an integer:
IntUnaryOperator unaryOperator = (num -> num * 100);
unaryOperator.applyAsInt(100); // 10000
Another example of operators is BinaryOperator<T>
, it has no abstract method but it extends the BiFunction<T,T,T>
interface which is a functional interface hence this BinaryOperator<T>
is also a functional interface and can be targeted using lambda expression:
BinaryOperator<String> binaryOperator = (str1, str2) -> str1 + " is " + str2;
binaryOperator.apply("coding", "...I don't know"); // coding is ...I don't know
This lambda expression will override a single abstract method on its parent, which is apply()
, it takes 2 strings and produce a string.
There are other operators: DoubleUnaryOperator, IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator, etc…
Legacy Functional Interfaces
As briefly mentioned at the beginning of this article, there are some interfaces with only one abstract method having been existing before Java 8 also are functional interfaces. Some of them are Runnable
, Callable
, they can be used as lambda expressions and marked with @FunctionalInterface annotation:
Thread myThread = new Thread(() -> System.out.println("Hello world from another thread"));
myThread.start();
Conclusion
Each functional interface just contains only one abstract method, thus they can be used as lambda expressions. More and more functional interfaces are being added to Java which tells us the elegance of the functional programming approach. The functional interface is a great concept to grasp in Java, having a good understanding of this concept and knowing how to use these predefined functional interfaces will make your life much easier. If you enjoy reading my post, consider subscribing to my newsletters to receive weekly updates.