Java Functional Interfaces: A Comprehensive Guide with Examples

Learn about Java functional interfaces, introduced in Java 8. Discover how they simplify code and enable functional programming. This guide includes examples and explanations of key concepts, such as lambda expressions and method references.


Java - Functional Interfaces

Functional interfaces were introduced in Java 8 along with lambda expressions and method references. These features significantly improved functional programming in Java, making the code cleaner and more readable. Before Java 8, developers had to write a lot of boilerplate code to perform basic tasks, such as creating a class and its instances to call a function. Lambda expressions simplify this process by allowing you to directly implement functional interfaces without needing concrete or anonymous class objects.

What is a Functional Interface?

A functional interface contains exactly one abstract method but can include any number of default or static methods. For example, the Comparable interface with its single compareTo() method is used for comparisons. Java 8 introduced many functional interfaces, making lambda expressions more efficient to use.

@FunctionalInterface Annotation

By definition, any interface with a single abstract method is considered a functional interface. The @FunctionalInterface annotation is optional but helps the compiler enforce the rule that only one abstract method is present, increasing readability and maintainability of the code.

Types of Functional Interfaces in Java

There are four primary types of functional interfaces in Java:

1. Predicate Functional Interface

A predicate functional interface has a method that accepts one argument and returns either true or false. It's commonly used for comparisons and filtering elements. Java provides predicate interfaces for primitive types like IntPredicate, DoublePredicate, and LongPredicate.

Example: Filtering Even Numbers

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class Tester {
   public static void main(String args[]) {
      List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
      Predicate isEvenNumber = n -> n % 2 == 0;
      numbers = numbers.stream().filter(isEvenNumber).toList();
      System.out.println(numbers);
   }
}
            
Output

[2, 4, 6, 8]
            

2. Consumer Functional Interface

A consumer functional interface accepts one argument and does not return anything. It is mainly used for operations that produce side effects, like printing or modifying elements. Java also provides primitive-specific consumers such as IntConsumer, DoubleConsumer, and LongConsumer.

Example: Printing Elements

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

public class Tester {
   public static void main(String args[]) {
      List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
      Consumer consumer = (value) -> System.out.println(value);
      Consumer consumer1 = System.out::println;

      System.out.println("Using lambda expression:");
      numbers.forEach(consumer);

      System.out.println("Using method reference:");
      numbers.forEach(consumer1);
   }
}
            
Output

Using lambda expression:
1
2
3
4
5
6
7
8
Using method reference:
1
2
3
4
5
6
7
8
            

3. Supplier Functional Interface

A supplier functional interface does not accept any arguments but returns a value. It's typically used to generate values lazily, such as fetching random numbers or generating a sequence.

Example: Generating Random Numbers

import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;

public class Tester {
   public static void main(String args[]) {
      Supplier supplier = () -> (int)(Math.random() * 10);
      List randomNumbers = new ArrayList<>();

      for (int i = 0; i < 10; i++) {
         randomNumbers.add(supplier.get());
      }
      System.out.println(randomNumbers);
   }
}
            
Output

[0, 8, 8, 8, 8, 5, 7, 5, 5, 9]
            

4. Function Functional Interface

The function functional interface accepts one argument and returns a result. It's useful for transforming data, such as calculating the square of numbers. Java also provides specific versions for primitive types like IntFunction, DoubleFunction, and LongFunction.

Example: Squaring Numbers

import java.util.Arrays;
import java.util.List;
import java.util.function.Function;

public class Tester {
   public static void main(String args[]) {
      List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
      Function squared = (value) -> value * value;
      List squaredNumbers = numbers.stream().map(squared).toList();
      System.out.println(squaredNumbers);
   }
}
            
Output

[1, 4, 9, 16, 25, 36, 49, 64]
            

Existing Functional Interfaces Prior to Java 8

Several interfaces already existed before Java 8 that now qualify as functional interfaces. For example:

  • Runnable − Provides the run() method.
  • Callable − Provides the call() method.
  • ActionListener − Provides the actionPerformed() method.
  • Comparable − Provides the compareTo() method for comparing two elements.

Example: Runnable Interface

Example: Runnable Interface with Lambda

public class Tester {
   public static void main(String args[]) {
      // Anonymous inner class
      new Thread(new Runnable() {
         @Override
         public void run() {
            System.out.println("Thread 1 is running");
         }
      }).start();

      // Lambda expression
      new Thread(() -> System.out.println("Thread 2 is running")).start();
   }
}
            
Output

Thread 1 is running
Thread 2 is running