A Guide To Functional Interfaces in Java10 min read

A functional interface is an interface that contains exactly one abstract method. When talking about a functional interface, people often think about Java 8 because this concept originally emerged at that time, along with many 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 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 called a functional interface, not just these predefined interfaces. Before Java 8, there were 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.

Create your own functional interface

We can create our own functional interface; let’s demonstrate that by creating 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 shortcoming if we discussed functional interfaces but forgot to mention lambda expressions. Before Java 8, because we could not directly create a new instance from an interface, the feasible solutions at that time were to create a concrete class with a lot of unnecessary boilerplate just to 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 more 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 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 interface type on the left and the type of the lambda expression have the same semantic. 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 the 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 the 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, corresponding 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 fits to the interface declaration Function<String, Integer>.

Another interface BiFunction<T, U, R> represents a functional that accepts two arguments and produces 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 two arguments and returns a result type R. Let’s implement a simple BiFunction<T, U, R> interface, which accepts two integers and returns 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 belonging 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 doesn’t accept anything and returns 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 must include the parentheses when there is no argument in the lambda expression syntax. There are other suppliers: LongSupplier, IntSupplier, etc..

Consumers

Interfaces belonging 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 returns 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 four elements.
  • Right below, we create a consumer that takes the parameterized type of List<String>, which is the type of names variable which we have defined.
  • On the right-hand side of this consumer, we implement the Consumer interface by overriding the accept() method, here we pass in a list with type of List<String>, iterating through each element of this list and modifying each element.
  • Then we call the accept() method on this consumer, pass names with the type of List<String> to it.
  • At the bottom, we print each element of this modified list with the forEach method, interestingly, this method also accepts a Consumer; each element on the list will override the accept() method; again, it doesn’t return anything and just prints out each element on the list.

The list has now 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 one or two operands and returns the result with the same type as their operand. For example, the IntUnaryOperator has an abstract method int applyAsInt(int num) which accepts an integer and returns 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 produces 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 existed before Java 8, which are also 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();
Previous Article
Next Article
Every support is much appreciated ❤️

Buy Me a Coffee