Skip to content
Java java8 6 min read

Functional Interfaces

A functional interface is simply an interface that has exactly one abstract method. That single-method contract is what lets Java treat a lambda expression or a method reference as an instance of the interface — no anonymous class boilerplate required.

What Makes an Interface “Functional”?

The rule is strict: one abstract method, no more. An interface can still have any number of default or static methods and remain functional. Java 8 introduced the @FunctionalInterface annotation to make the contract explicit and compiler-enforced.

@FunctionalInterface
interface Greeter {
    String greet(String name);  // the one abstract method
}

If you accidentally add a second abstract method, the compiler immediately rejects the annotation:

@FunctionalInterface
interface Broken {
    void doA();
    void doB(); // compile error: multiple non-overriding abstract methods
}

Tip: Always add @FunctionalInterface to your own single-method interfaces. It’s free documentation and a compile-time safety net.

Using a Functional Interface with a Lambda

Before lambdas, you’d implement a single-method interface with an anonymous class. Lambdas collapse that into one line:

@FunctionalInterface
interface MathOperation {
    int operate(int a, int b);
}

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

        System.out.println("5 + 3 = " + add.operate(5, 3));
        System.out.println("5 * 3 = " + multiply.operate(5, 3));
    }
}

Output:

5 + 3 = 8
5 * 3 = 15

The lambda (a, b) -> a + b is just a concise implementation of MathOperation. The compiler infers the target type from the variable declaration.

Built-In Functional Interfaces (java.util.function)

Java 8 ships a rich set of ready-to-use functional interfaces in the java.util.function package so you rarely need to write your own. They fall into four families:

InterfaceSignaturePurpose
Predicate<T>boolean test(T t)Test a condition
Function<T, R>R apply(T t)Transform T into R
Consumer<T>void accept(T t)Consume a value (side-effect)
Supplier<T>T get()Produce a value
BiFunction<T,U,R>R apply(T t, U u)Transform two inputs into one output
UnaryOperator<T>T apply(T t)Function where input and output are the same type
BinaryOperator<T>T apply(T t1, T t2)BiFunction where all types are the same

Predicate — testing a condition

import java.util.function.Predicate;

public class PredicateDemo {
    public static void main(String[] args) {
        Predicate<Integer> isEven = n -> n % 2 == 0;

        System.out.println(isEven.test(4));   // true
        System.out.println(isEven.test(7));   // false

        // combine predicates
        Predicate<Integer> isPositive = n -> n > 0;
        Predicate<Integer> isPositiveEven = isEven.and(isPositive);
        System.out.println(isPositiveEven.test(6));   // true
        System.out.println(isPositiveEven.test(-2));  // false
    }
}

Output:

true
false
true
false

Predicate is used heavily in Stream operations like .filter().

Function — transforming a value

import java.util.function.Function;

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

        System.out.println(strLength.apply("Hello"));   // 5
        System.out.println(strLength.apply("DevCraftly")); // 10

        // chain with andThen
        Function<Integer, String> intToLabel = n -> "Length: " + n;
        Function<String, String> lengthLabel = strLength.andThen(intToLabel);
        System.out.println(lengthLabel.apply("Java"));  // Length: 4
    }
}

Output:

5
10
Length: 4

Consumer — performing side effects

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

public class ConsumerDemo {
    public static void main(String[] args) {
        Consumer<String> printUpper = s -> System.out.println(s.toUpperCase());

        List<String> names = List.of("alice", "bob", "carol");
        names.forEach(printUpper);
    }
}

Output:

ALICE
BOB
CAROL

Supplier — producing a value lazily

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

public class SupplierDemo {
    public static void main(String[] args) {
        Supplier<LocalDate> today = () -> LocalDate.now();
        System.out.println("Today is: " + today.get());
    }
}

Output:

Today is: 2026-06-13

Supplier is perfect for lazy initialization — the lambda body runs only when get() is called, not when the variable is assigned.

Primitive Specializations

Boxing a primitive like int into Integer every time you pass it through a Function<Integer, Integer> wastes memory and CPU. Java provides primitive specializations to avoid that overhead:

GenericPrimitive equivalent
Function<Integer, Integer>IntUnaryOperator
Predicate<Integer>IntPredicate
Consumer<Integer>IntConsumer
Supplier<Integer>IntSupplier
Function<T, Integer>ToIntFunction<T>
import java.util.function.IntPredicate;

public class PrimitiveDemo {
    public static void main(String[] args) {
        IntPredicate isOdd = n -> n % 2 != 0;
        System.out.println(isOdd.test(9));  // true
        System.out.println(isOdd.test(8));  // false
    }
}

Tip: Prefer primitive specializations in hot loops or large stream pipelines — they eliminate autoboxing entirely.

Composing Functions

The Function, Predicate, and Consumer interfaces expose default methods for building pipelines without nesting:

import java.util.function.Function;

public class ComposeDemo {
    public static void main(String[] args) {
        Function<Integer, Integer> doubleIt  = x -> x * 2;
        Function<Integer, Integer> addThree  = x -> x + 3;

        // andThen: doubleIt first, then addThree
        Function<Integer, Integer> doubleThenAdd = doubleIt.andThen(addThree);
        System.out.println(doubleThenAdd.apply(5)); // (5*2)+3 = 13

        // compose: addThree first, then doubleIt
        Function<Integer, Integer> addThenDouble = doubleIt.compose(addThree);
        System.out.println(addThenDouble.apply(5)); // (5+3)*2 = 16
    }
}

Output:

13
16
  • andThen(g) — apply this, then g
  • compose(g) — apply g first, then this

Writing Your Own Functional Interface

Sometimes the built-in interfaces don’t match your domain. Writing a custom one is trivial:

@FunctionalInterface
interface Transformer<T> {
    T transform(T input);

    // default helper — still functional (only one abstract method)
    default Transformer<T> andThen(Transformer<T> after) {
        return input -> after.transform(this.transform(input));
    }
}

public class CustomFIDemo {
    public static void main(String[] args) {
        Transformer<String> trim    = String::trim;
        Transformer<String> upper   = String::toUpperCase;
        Transformer<String> pipeline = trim.andThen(upper);

        System.out.println(pipeline.transform("  hello world  "));
    }
}

Output:

HELLO WORLD

Notice how method references like String::trim satisfy the interface because trim() matches the T transform(T input) signature.

Under the Hood

How the JVM represents lambdas

When the compiler sees a lambda, it does not generate a new .class file the way an anonymous class would. Instead it uses the invokedynamic bytecode instruction (introduced in Java 7) together with LambdaMetafactory. At runtime, the JVM creates a class on the fly — once, on first call — and caches it. Subsequent calls reuse the same instance when the lambda captures no state (non-capturing lambdas). This makes lambdas substantially cheaper than old-style anonymous classes.

Target typing

The compiler resolves which functional interface a lambda should become based on context — the variable type, method parameter type, or cast. This is called target typing. A single lambda body x -> x * 2 can become an IntUnaryOperator, a Function<Integer, Integer>, or any other compatible functional interface depending on where it appears.

@FunctionalInterface at the bytecode level

The annotation itself has @Retention(RUNTIME), so it is also visible via reflection. Frameworks can query method.getAnnotation(FunctionalInterface.class) to verify a type at runtime. The JDK’s own java.util.function types all carry it.

Note: Runnable, Callable<V>, Comparator<T>, and ActionListener all predate Java 8 but are valid functional interfaces — they each have exactly one abstract method and work seamlessly with lambdas.

Quick Reference

TaskInterface to reach for
Filter a listPredicate<T>
Map one type to anotherFunction<T, R>
Print / log / save a valueConsumer<T>
Generate / lazy-load a valueSupplier<T>
Combine two values into oneBiFunction<T,U,R>
Sort objectsComparator<T>
Run a task in a threadRunnable
Return a result from a threadCallable<V>
Last updated June 13, 2026
Was this helpful?