00:00

Functional Interface in Java – Lambdas and java.util.function

A functional interface is an interface with exactly one abstract method (a "single abstract method", or SAM, type). Because there is only one method to implement, Java lets you supply the implementation as a compact lambda expression or method reference instead of a whole class.

Functional interfaces arrived with lambdas in Java 8 and power streams, Optional, CompletableFuture and most modern Java APIs. This tutorial explains the rules, the @FunctionalInterface annotation, and the built-in interfaces in java.util.function.

1. What Makes an Interface Functional?

  • It has exactly one abstract method.
  • It may have any number of default and static methods.
  • Abstract methods that match public methods of Object (equals, hashCode, toString) do not count; this is why Comparator, which declares equals, is still functional.

Classic examples existed before Java 8: Runnable (run()), Callable (call()), Comparator (compare()) and ActionListener.

2. Creating a Functional Interface

CustomFunctional.java
public class CustomFunctional {
    @FunctionalInterface
    interface Calculator {
        int compute(int a, int b);                 // the single abstract method

        default Calculator andThenDouble() {       // default methods are allowed
            return (a, b) -> compute(a, b) * 2;
        }
        static Calculator add() { return (a, b) -> a + b; }   // static methods too
        boolean equals(Object o);                 // Object methods do not count
    }

    public static void main(String[] args) {
        // Three ways to implement it
        Calculator multiply = new Calculator() {          // 1. anonymous class
            @Override public int compute(int a, int b) { return a * b; }
        };
        Calculator subtract = (a, b) -> a - b;            // 2. lambda
        Calculator max = Math::max;                       // 3. method reference

        System.out.println("multiply: " + multiply.compute(6, 7));
        System.out.println("subtract: " + subtract.compute(10, 4));
        System.out.println("max:      " + max.compute(3, 9));
        System.out.println("add x2:   " + Calculator.add().andThenDouble().compute(2, 3));
    }
}
Output
multiply: 42
subtract: 6
max:      9
add x2:   10

The same interface is implemented three ways. The lambda (a, b) -> a - b is converted into an object of Calculator, because the compiler knows compute is the only method it can stand for.

3. The @FunctionalInterface Annotation

@FunctionalInterface is optional, but it documents intent and makes the compiler check the rule. Adding a second abstract method then causes an error:

Compile error
@FunctionalInterface
interface Validator {
    boolean validate(String s);
    boolean validateAll(String... s);   // error: multiple non-overriding abstract methods
}

Any interface with one abstract method can be used with lambdas, annotated or not.

4. Built-in Functional Interfaces (java.util.function)

Java provides 43 ready-made functional interfaces, so you rarely need your own. The core ones:

InterfaceMethodTakes → ReturnsTypical use
Predicate<T>test(T)T → booleanFiltering, conditions
Function<T,R>apply(T)T → RTransforming, mapping
Consumer<T>accept(T)T → nothingPrinting, saving
Supplier<T>get()nothing → TLazy values, factories
UnaryOperator<T>apply(T)T → TModifying a value
BinaryOperator<T>apply(T,T)(T, T) → TReducing, combining
BiFunction<T,U,R>apply(T,U)(T, U) → RTwo-argument transforms

Primitive versions such as IntPredicate, ToIntFunction, IntUnaryOperator and LongSupplier avoid boxing overhead.

BuiltInFunctional.java
import java.util.function.*;

public class BuiltInFunctional {
    public static void main(String[] args) {
        Predicate<String> isLong   = s -> s.length() > 5;
        Function<String, Integer> length = String::length;
        Consumer<String> printer   = s -> System.out.println("  consumed: " + s);
        Supplier<String> greeting  = () -> "Hello from a Supplier";
        UnaryOperator<String> upper = String::toUpperCase;
        BinaryOperator<Integer> sum = Integer::sum;
        BiFunction<String, Integer, String> repeat = String::repeat;

        System.out.println("Predicate  isLong(\"Java\"):       " + isLong.test("Java"));
        System.out.println("Function   length(\"Kafka\"):      " + length.apply("Kafka"));
        printer.accept("Spring Boot");
        System.out.println("Supplier   get():                " + greeting.get());
        System.out.println("UnaryOp    upper(\"java\"):        " + upper.apply("java"));
        System.out.println("BinaryOp   sum(4, 5):            " + sum.apply(4, 5));
        System.out.println("BiFunction repeat(\"ab\", 3):      " + repeat.apply("ab", 3));

        // Primitive specialisations avoid boxing
        IntPredicate even = n -> n % 2 == 0;
        IntUnaryOperator square = n -> n * n;
        System.out.println("IntPredicate even(10):           " + even.test(10));
        System.out.println("IntUnaryOperator square(12):     " + square.applyAsInt(12));
    }
}
Output
Predicate  isLong("Java"):       false
Function   length("Kafka"):      5
  consumed: Spring Boot
Supplier   get():                Hello from a Supplier
UnaryOp    upper("java"):        JAVA
BinaryOp   sum(4, 5):            9
BiFunction repeat("ab", 3):      ababab
IntPredicate even(10):           true
IntUnaryOperator square(12):     144

5. Composing Functions and Passing Behaviour

Built-in interfaces include default methods for combining them: Predicate.and/or/negate, Function.andThen/compose and Consumer.andThen. Methods can also accept functional interfaces as parameters, passing behaviour like data:

ComposingFunctions.java
import java.util.List;
import java.util.function.*;

public class ComposingFunctions {
    public static void main(String[] args) {
        Predicate<String> notBlank = s -> !s.isBlank();
        Predicate<String> startsWithJ = s -> s.startsWith("J");
        Predicate<String> valid = notBlank.and(startsWithJ.negate());   // non-blank AND not starting with "J"

        Function<String, String> trim = String::trim;
        Function<String, String> clean = trim.andThen(String::toLowerCase).andThen(s -> s.replace(' ', '-'));

        List<String> titles = List.of("  Spring Boot Basics ", "Java Streams", "  ", "Kafka Consumers");
        titles.stream()
              .filter(valid)
              .map(clean)
              .forEach(slug -> System.out.println("slug: " + slug));

        // Functional interfaces as parameters: behaviour passed like data
        System.out.println("twice(+3) on 10 = " + twice(n -> n + 3, 10));
    }

    static int twice(IntUnaryOperator op, int value) {
        return op.applyAsInt(op.applyAsInt(value));
    }
}
Output
slug: spring-boot-basics
slug: kafka-consumers
twice(+3) on 10 = 16

6. Rules and Notes

  • A lambda's parameter and return types must match the abstract method of the target interface.
  • Lambdas can use local variables only if they are final or effectively final.
  • A functional interface can extend another interface if the result still has one abstract method.
  • Inside a lambda, this refers to the enclosing object, unlike an anonymous class.
  • Checked exceptions: built-in interfaces like Function do not declare any, so lambdas must handle them or you define your own interface with a throws clause.

7. Summary

  • A functional interface has exactly one abstract method; default and static methods are allowed.
  • It can be implemented with a lambda, a method reference or an anonymous class.
  • @FunctionalInterface asks the compiler to enforce the rule.
  • java.util.function provides Predicate, Function, Consumer, Supplier and many more.
  • Default methods such as andThen and negate let you compose behaviour.

Related Tutorials