Functional Interfaces

July 6, 20264 min readUpdated 8/20/2026

A functional interface is an interface with exactly one abstract method. That single rule is what makes lambdas work: when the compiler sees a lambda, it needs to know which method the lambda is implementing, and one abstract method means there is no ambiguity.

The rule, and the annotation

@FunctionalInterface
interface Validator {
    boolean test(String input);          // exactly one abstract method

    default Validator and(Validator other) {   // default methods do not count
        return s -> this.test(s) && other.test(s);
    }

    static Validator notNull() {               // static methods do not count either
        return s -> s != null;
    }
}

@FunctionalInterface is optional but worth adding. It makes the compiler reject a second abstract method, so nobody can break every lambda that uses your interface by adding one later. Without it the interface still works with lambdas — the annotation documents the intent and enforces it.

Note what does not count against the one-method limit: default methods, static methods, and public methods inherited from Object such as equals or toString.

They were already there before Java 8

The idea is older than the syntax. Several interfaces you have used for years qualify, which is why they work with lambdas without anyone changing them:

Runnable job = () -> System.out.println("working");        // run()
Comparator<String> byLength = (a, b) -> a.length() - b.length();   // compare()

class Demo {
    void run() {
        new Thread(job).start();
        List<String> names = new ArrayList<>(List.of("Christopher", "Bo"));
        names.sort(byLength);
        System.out.println(names);                          // [Bo, Christopher]
    }
}

Java 8 did not invent functional interfaces. It added a compact way to implement them and a package full of ready-made ones.

The java.util.function package

Before writing your own, check whether one of these fits. These six cover almost everything:

InterfaceMethodShapeUsed for
Predicate<T>testT → booleanfiltering, conditions
Function<T,R>applyT → Rtransforming
Consumer<T>acceptT → voiddoing something with each item
Supplier<T>get() → Tproducing a value lazily
UnaryOperator<T>applyT → Ttransforming without changing type
BiFunction<T,U,R>applyT, U → Rcombining two things
Predicate<String> isLong = s -> s.length() > 5;
Function<String, Integer> length = String::length;
Consumer<String> print = System.out::println;
Supplier<LocalDate> today = LocalDate::now;
UnaryOperator<String> shout = s -> s.toUpperCase() + "!";
BiFunction<Integer, Integer, Integer> add = Integer::sum;

class Demo {
    void run() {
        System.out.println(isLong.test("hello"));      // false
        System.out.println(length.apply("hello"));     // 5
        System.out.println(shout.apply("hey"));        // HEY!
        System.out.println(add.apply(20, 22));         // 42
        print.accept("done");
    }
}

There are primitive variants too — IntPredicate, ToIntFunction, DoubleSupplier and a couple of dozen more. They exist to avoid boxing every value into an Integer, which matters when you are processing millions of elements and is irrelevant otherwise.

Composing them

This is where they stop being a curiosity. The built-in interfaces carry default methods that build new ones out of old ones:

Predicate<String> notBlank = s -> !s.isBlank();
Predicate<String> isShort = s -> s.length() < 10;

class Demo {
    void run() {
        Predicate<String> valid = notBlank.and(isShort);
        System.out.println(valid.test("hello"));        // true
        System.out.println(valid.test(""));             // false
        System.out.println(notBlank.negate().test("")); // true
        System.out.println(notBlank.or(isShort).test("")); // true

        Function<String, String> trim = String::strip;
        Function<String, Integer> len = String::length;
        System.out.println(trim.andThen(len).apply("  hi  "));   // 2
        System.out.println(len.compose(trim).apply("  hi  "));   // 2 — same, other order
    }
}

andThen runs this one first, then the argument. compose is the reverse. When you cannot remember which is which, use andThen — it reads left to right.

Writing your own

Write one when the built-ins do not express the intent, when you need a name that means something in your domain, or when you want more than two parameters:

@FunctionalInterface
interface Discount {
    double applyTo(double price);
}

@FunctionalInterface
interface TriFunction<A, B, C, R> {          // the JDK stops at two arguments
    R apply(A a, B b, C c);
}

class Checkout {
    double checkout(double price, Discount discount) {
        return discount.applyTo(price);
    }

    void run() {
        System.out.println(checkout(100, p -> p * 0.9));   // 90.0
        System.out.println(checkout(100, p -> p - 15));    // 85.0

        TriFunction<Integer, Integer, Integer, Integer> sum3 = (a, b, c) -> a + b + c;
        System.out.println(sum3.apply(1, 2, 3));           // 6
    }
}

Discount is the better example of the two. Function<Double, Double> would work identically and say nothing; Discount tells the next reader what the parameter is for, and applyTo reads better at the call site than apply.

Where they show up

Once you recognise the shape, you see it everywhere in the standard library — and you can read those signatures instead of guessing:

List<String> names = new ArrayList<>(List.of("Ana", "Bo"));
Map<String, Integer> ages = new HashMap<>();

class Demo {
    void run(List<String> names, Map<String, Integer> ages) {
        names.removeIf(s -> s.isBlank());          // Predicate
        names.replaceAll(String::toUpperCase);     // UnaryOperator
        names.forEach(System.out::println);        // Consumer
        ages.computeIfAbsent("Ana", k -> 0);       // Function
        ages.merge("Ana", 1, Integer::sum);        // BiFunction
        Optional.empty().orElseGet(() -> "none");  // Supplier
    }
}

Two things that catch people

A lambda is not an object of the interface's "type" in any special sense. It is an instance of some class the runtime supplies, so identity comparisons on lambdas are meaningless and two identical lambdas are not equals:

Predicate<String> a = s -> s.isEmpty();
Predicate<String> b = s -> s.isEmpty();

class Demo {
    void run(Predicate<String> a, Predicate<String> b) {
        System.out.println(a == b);        // false
        System.out.println(a.equals(b));   // false — do not store lambdas in a Set and expect dedup
    }
}

An interface with two abstract methods cannot take a lambda, and the compiler error names the count rather than the fix. If you meant one of them to have a body, make it default:

interface Broken {
    boolean test(String s);
    String describe();                    // now Broken is not functional
}

@FunctionalInterface
interface Fixed {
    boolean test(String s);
    default String describe() {           // a body, so it no longer counts
        return "a test";
    }
}

Next

A lambda that does nothing but call one existing method can be written more directly still. Method References is next.