A functional interface in Java is an interface that contains exactly one abstract method. Introduced in Java 8, they are the backbone of lambda expressions and the Stream API, enabling a more functional style of programming. They are also known as Single Abstract Method (SAM) interfaces.

To ensure an interface is a functional interface, you can use the @FunctionalInterface annotation. The compiler will then trigger an error if the interface does not meet the requirements.

Here’s a simple example:

@FunctionalInterface
interface MyFunctionalInterface {
    void execute(String message); // The single abstract method
}

public class FunctionalInterfaceExample {
    public static void main(String[] args) {
        // Using a lambda expression to implement the interface
        MyFunctionalInterface myLambda = (msg) -> System.out.println("Executing: " + msg);
        myLambda.execute("Hello, Functional World!");
    }
}

Core Functional Interfaces in java.util.function

Java 8 introduced a set of standard functional interfaces in the java.util.function package. These are widely used and cover most common use cases. Let’s explore the four main ones.

1. Consumer<T>

A Consumer “consumes” an input value and performs an operation on it without returning any result.

  • Abstract Method: void accept(T t)
  • Use Case: Ideal for operations with side effects, such as printing to the console, logging, or modifying an object.

Example:

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

public class ConsumerExample {
    public static void main(String[] args) {
        Consumer<String> printer = s -> System.out.println(s);
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        names.forEach(printer); // Prints each name
    }
}

2. Predicate<T>

A Predicate evaluates a condition on an input value and returns a boolean result.

  • Abstract Method: boolean test(T t)
  • Use Case: Perfect for filtering data, validation, and conditional checks. It’s heavily used in the Stream API’s filter() method.

Example:

import java.util.function.Predicate;
import java.util.stream.Stream;

public class PredicateExample {
    public static void main(String[] args) {
        Predicate<String> isLongerThan5 = s -> s.length() > 5;
        Stream.of("Java", "Python", "JavaScript")
              .filter(isLongerThan5)
              .forEach(System.out::println); // Prints "JavaScript"
    }
}

3. Function<T, R>

A Function takes an input of type T and produces a result of type R. It’s used for transformation and mapping.

  • Abstract Method: R apply(T t)
  • Use Case: Transforming one object into another, such as converting a String to its length or mapping a DTO to an entity.

Example:

import java.util.function.Function;
import java.util.stream.Stream;

public class FunctionExample {
    public static void main(String[] args) {
        Function<String, Integer> stringLength = s -> s.length();
        Stream.of("Apple", "Banana", "Cherry")
              .map(stringLength)
              .forEach(System.out::println); // Prints 5, 6, 6
    }
}

4. Supplier<T>

A Supplier “supplies” a value of type T without taking any input.

  • Abstract Method: T get()
  • Use Case: Useful for lazy generation of values, such as creating new objects, generating random numbers, or providing default values.

Example:

import java.util.function.Supplier;
import java.time.LocalDateTime;

public class SupplierExample {
    public static void main(String[] args) {
        Supplier<LocalDateTime> currentTimeSupplier = () -> LocalDateTime.now();
        System.out.println(currentTimeSupplier.get()); // Prints the current date and time
    }
}

By understanding and using these core functional interfaces, you can write more expressive, concise, and powerful Java code.