Java Functional Interfaces

A functional interface is an interface that contains exactly one abstract method. Functional interfaces are the foundation of lambda expressions and functional programming in Java.

Java provides several built-in functional interfaces in the java.util.function package, making it easy to pass behavior as a method argument.

Creating a Functional Interface

The @FunctionalInterface annotation tells the compiler that an interface is intended to contain one abstract method.

Java
A simple custom functional interface.
@FunctionalInterface
interface Calculator {
    int calculate(int a, int b);
}

public class Main {
    public static void main(String[] args) {
        Calculator add = (a, b) -> a + b;
        Calculator multiply = (a, b) -> a * b;

        System.out.println(add.calculate(10, 5));
        System.out.println(multiply.calculate(10, 5));
    }
}

Predicate

Predicate accepts one argument and returns a boolean. It is commonly used for filtering and validation.

Java
Using Predicate to check whether a number is positive.
import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        Predicate<Integer> positive = number -> number > 0;

        System.out.println(positive.test(10));
        System.out.println(positive.test(-5));
    }
}

Consumer

Consumer accepts one argument and does not return a value. It is useful for operations such as printing, logging, or updating data.

Java
Using Consumer to print a value.
import java.util.function.Consumer;

public class Main {
    public static void main(String[] args) {
        Consumer<String> printer = text -> System.out.println(text);

        printer.accept("Hello Java");
    }
}

Supplier

Supplier does not accept arguments but returns a value. It is useful when a value needs to be generated lazily.

Java
Using Supplier to generate a value.
import java.util.function.Supplier;

public class Main {
    public static void main(String[] args) {
        Supplier<String> message = () -> "Hello from Supplier";

        System.out.println(message.get());
    }
}

Function

Function accepts a value of type T and returns a value of type R. It is commonly used for transformations.

Java
Converting a String into its length.
import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Function<String, Integer> length = text -> text.length();

        System.out.println(length.apply("Java"));
    }
}

BiFunction

BiFunction accepts two arguments and returns a result.

Java
Adding two integers using BiFunction.
import java.util.function.BiFunction;

public class Main {
    public static void main(String[] args) {
        BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;

        System.out.println(add.apply(10, 20));
    }
}

UnaryOperator

UnaryOperator is a specialized Function where the input and output have the same type.

Java
Doubling an integer using UnaryOperator.
import java.util.function.UnaryOperator;

public class Main {
    public static void main(String[] args) {
        UnaryOperator<Integer> doubleValue = number -> number * 2;

        System.out.println(doubleValue.apply(10));
    }
}

BinaryOperator

BinaryOperator is a specialized BiFunction where both inputs and the result have the same type.

Java
Finding the larger number using BinaryOperator.
import java.util.function.BinaryOperator;

public class Main {
    public static void main(String[] args) {
        BinaryOperator<Integer> max = Integer::max;

        System.out.println(max.apply(10, 25));
    }
}

Composing Functions

Function interfaces provide methods such as andThen and compose, allowing multiple transformations to be combined.

Java
Combining two functions into a single operation.
import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Function<String, String> trim = String::trim;
        Function<String, String> upper = String::toUpperCase;

        Function<String, String> process = trim.andThen(upper);

        System.out.println(process.apply("  hello java  "));
    }
}

When to Create a Custom Functional Interface

Standard interfaces should be preferred when they clearly describe the required behavior. Create a custom functional interface when the operation has domain-specific meaning or needs a clearer API.

Java
A domain-specific functional interface.
@FunctionalInterface
interface PriceCalculator {
    double calculate(double price);
}

public class Main {
    public static void main(String[] args) {
        PriceCalculator discount = price -> price * 0.90;

        System.out.println(discount.calculate(1000));
    }
}

Common Functional Interfaces

TEXT
Quick reference for common functional interfaces.
Predicate<T>       : T -> boolean
Consumer<T>        : T -> void
Supplier<T>        : () -> T
Function<T, R>     : T -> R
BiFunction<T,U,R>  : (T,U) -> R
UnaryOperator<T>   : T -> T
BinaryOperator<T>  : (T,T) -> T

Best Practices

Use standard functional interfaces when they accurately represent the operation. Keep lambda expressions small and readable, and use method references when they make the code clearer.

Use @FunctionalInterface for custom functional interfaces so the compiler can verify that the interface contains only one abstract method.

Conclusion

Functional interfaces are a core part of modern Java. Predicate, Consumer, Supplier, Function, and their specialized variants allow behavior to be passed around as values and work naturally with lambda expressions and streams.