Skip to content

Java Functional Interfaces

Updated 10 min read Aimerscrew

A Java functional interface is an interface with exactly one abstract method. That single "slot" is what a lambda fills in: when you write x -> x * 2, Java needs to know which method that code belongs to, and a functional interface answers the question. In this lesson, you'll learn the rules, write your own functional interface with the @FunctionalInterface annotation, get to know the built-in ones in java.util.function, and combine them into bigger rules with and, negate, andThen and compose.

🌍 Real-life example

Think of a power socket. The socket defines one thing: the shape of the plug it accepts. A lamp, a phone charger or a kettle can all plug in, as long as the plug has that shape. A functional interface is the socket, and each lambda is a different appliance with the right plug.

What is a functional interface in Java?

A functional interface is any interface that has exactly one abstract method, often called its single abstract method (SAM). It may also contain any number of default and static methods, because those already have bodies.

You've already used some without knowing it:

  • Runnable has one method: void run().
  • Comparator<T> has one abstract method: int compare(T a, T b).
  • Comparable<T> has one: int compareTo(T other).

Because each has exactly one abstract method, you can create any of them with a lambda expression.

How to write your own functional interface

Let's build an online store where marketing keeps inventing new discount rules. Every rule takes a price and returns a new price, so that's our single abstract method:

public class Checkout {
    public static void main(String[] args) {
        DiscountRule noDiscount = price -> price;
        DiscountRule blackFriday = price -> price * 0.70;
        DiscountRule fiveOff = price -> Math.max(0, price - 5);

        double cartTotal = 60.00;
        System.out.printf("Regular:      $%.2f%n", noDiscount.apply(cartTotal));
        System.out.printf("Black Friday: $%.2f%n", blackFriday.apply(cartTotal));
        System.out.printf("$5 coupon:    $%.2f%n", fiveOff.apply(cartTotal));
        System.out.println(blackFriday.describe(cartTotal));
    }
}

@FunctionalInterface
interface DiscountRule {
    double apply(double price);   // the single abstract method

    default String describe(double price) {   // default methods are allowed
        return String.format("You save $%.2f", price - apply(price));
    }
}
Regular:      $60.00
Black Friday: $42.00
$5 coupon:    $55.00
You save $18.00

Three different rules, zero extra classes. Each lambda becomes the body of apply. The describe method is a default method, so it doesn't count against the "one abstract method" limit, and every rule gets it for free.

What does @FunctionalInterface do?

The @FunctionalInterface annotation is optional, but it's a promise checked by the compiler. If someone later adds a second abstract method to DiscountRule, the code no longer compiles:

error: Unexpected @FunctionalInterface annotation
  DiscountRule is not a functional interface
    multiple non-overriding abstract methods found in interface DiscountRule

Without the annotation, the interface would quietly stop working with lambdas and the error would appear somewhere else, far from the real cause. So add it whenever you design an interface meant for lambdas.

📝 Note

Methods that every object already has, like equals(Object o) or toString(), don't count either. That's why Comparator, which also declares equals, is still a functional interface.

Built-in functional interfaces in java.util.function

You rarely need to write your own interface, because the package java.util.function already has one for almost every shape of function. Here are the ones you'll use most:

InterfaceMethodTakesReturnsTypical use
Predicate<T>test(t)one TbooleanA yes/no check: "is this seat free?"
Function<T, R>apply(t)one Tan RTurn one value into another
Consumer<T>accept(t)one TnothingDo something with a value (print, save)
Supplier<T>get()nothinga TProduce a value on demand
BiFunction<T, U, R>apply(t, u)a T and a Uan RCombine two different values
UnaryOperator<T>apply(t)one Ta TChange a value, same type
BinaryOperator<T>apply(a, b)two Tsa TMerge two values of the same type
Animated diagram of four core functional interfaces: Supplier takes nothing and get() returns 21; Consumer takes "Hi" and accept() returns nothing but prints; Function takes 16 and apply() returns "16 degrees C"; Predicate takes 16 and test() returns true or false
Pick the interface by its shape: what goes in, and what comes out.

Here's all seven running inside a smart thermostat:

import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;

public class SmartHome {
    public static void main(String[] args) {
        Predicate<Integer> isTooCold = temp -> temp < 18;
        Function<Integer, String> describe = temp -> temp + " degrees C";
        Consumer<String> display = text -> System.out.println("[Screen] " + text);
        Supplier<Integer> defaultTemp = () -> 21;
        BiFunction<String, Integer, String> setRoom = (room, temp) -> room + " set to " + temp;
        UnaryOperator<Integer> warmUp = temp -> temp + 2;
        BinaryOperator<Integer> average = (a, b) -> (a + b) / 2;

        int bedroom = 16;
        System.out.println("Too cold? " + isTooCold.test(bedroom));
        System.out.println(describe.apply(bedroom));
        display.accept("Good morning!");
        System.out.println("Default: " + defaultTemp.get());
        System.out.println(setRoom.apply("Kitchen", 20));
        System.out.println("After warm-up: " + warmUp.apply(bedroom));
        System.out.println("Average of 16 and 22: " + average.apply(16, 22));
    }
}
Too cold? true
16 degrees C
[Screen] Good morning!
Default: 21
Kitchen set to 20
After warm-up: 18
Average of 16 and 22: 19

Each interface has its own method name: test, apply, accept or get. You call that method to run the lambda stored inside.

💡 Tip

To choose the right interface, ask two questions: "How many values go in?" and "What comes out?". Nothing in, value out is a Supplier. Value in, nothing out is a Consumer. Value in, boolean out is a Predicate. Anything else is usually a Function or one of its relatives.

Passing behavior into your own methods

The real power appears when your methods accept a functional interface as a parameter. The method then works with any rule the caller gives it. This parking lot report filters cars by whatever condition you pass in:

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

public class ParkingLot {
    public static void main(String[] args) {
        List<Car> cars = List.of(
                new Car("ABC-123", true, 45),
                new Car("XYZ-789", false, 130),
                new Car("JKL-456", true, 200),
                new Car("MNO-321", false, 20));

        Predicate<Car> electric = Car::electric;
        Predicate<Car> longStay = car -> car.minutesParked() > 60;

        System.out.println("Electric:            " + plates(cars, electric));
        System.out.println("Electric, long stay: " + plates(cars, electric.and(longStay)));
        System.out.println("Electric or long:    " + plates(cars, electric.or(longStay)));
        System.out.println("Not electric:        " + plates(cars, electric.negate()));
    }

    // Our own method that accepts any rule as a Predicate
    static List<String> plates(List<Car> cars, Predicate<Car> rule) {
        List<String> result = new ArrayList<>();
        for (Car car : cars) {
            if (rule.test(car)) {
                result.add(car.plate());
            }
        }
        return result;
    }
}

record Car(String plate, boolean electric, int minutesParked) {
}
Electric:            [ABC-123, JKL-456]
Electric, long stay: [JKL-456]
Electric or long:    [ABC-123, XYZ-789, JKL-456]
Not electric:        [XYZ-789, MNO-321]

One method, plates, handles four different questions. Without functional interfaces, you'd write four nearly identical loops.

How do you combine predicates?

Predicate has default methods that build new predicates from existing ones, which you saw above:

  • a.and(b): true only when both are true (like &&).
  • a.or(b): true when at least one is true (like ||).
  • a.negate(): the opposite (like !).

Combining small, well-named predicates such as electric and longStay keeps each rule easy to read and test on its own.

Chaining functions with andThen and compose

Function can also be chained. f.andThen(g) runs f first and passes its result to g. f.compose(g) does the reverse: it runs g first, then f.

import java.util.function.Function;

public class PriceLabels {
    public static void main(String[] args) {
        Function<Double, Double> addTax = price -> price * 1.20;
        Function<Double, Double> roundUp = price -> Math.ceil(price);
        Function<Double, String> label = price -> "$" + price;

        Function<Double, String> pipeline = addTax.andThen(roundUp).andThen(label);
        System.out.println(pipeline.apply(9.10));

        // compose runs the OTHER function first
        Function<Double, Double> taxThenRound = roundUp.compose(addTax);
        Function<Double, Double> roundThenTax = roundUp.andThen(addTax);
        System.out.println(taxThenRound.apply(9.10));
        System.out.println(roundThenTax.apply(9.10));
    }
}
$11.0
11.0
12.0

Order matters. Adding 20% tax to $9.10 gives $10.92, which rounds up to 11. Rounding first gives 10, and 10 plus tax is 12. Think of andThen as "and then do this", which reads in the same order the steps happen.

Primitive versions: IntPredicate, IntBinaryOperator and friends

Generic interfaces like Predicate<Integer> work with objects, so every int gets boxed into an Integer (see wrapper classes). For number-heavy code, Java offers primitive versions such as IntPredicate, IntFunction, IntUnaryOperator, IntBinaryOperator, plus Long... and Double... variants.

import java.util.function.IntBinaryOperator;
import java.util.function.IntPredicate;

public class StepCounter {
    public static void main(String[] args) {
        IntPredicate reachedGoal = steps -> steps >= 10_000;
        IntBinaryOperator total = (morning, evening) -> morning + evening;

        int today = total.applyAsInt(6_200, 4_100);
        System.out.println("Steps today: " + today);
        System.out.println("Goal reached? " + reachedGoal.test(today));
    }
}
Steps today: 10300
Goal reached? true

The method names change slightly (applyAsInt instead of apply), but the idea is identical.

Common mistakes with functional interfaces

1. Adding a second abstract method

An interface with two abstract methods can't be the target of a lambda. Use @FunctionalInterface so the compiler catches this immediately.

2. Calling the wrong method name

isTooCold.apply(16) doesn't compile, because a Predicate's method is test. Remember: Predicate tests, Function applies, Consumer accepts, Supplier gets.

3. Mixing up andThen and compose

f.compose(g) runs g first. If the result looks backwards, you probably wanted andThen.

4. Writing your own interface when a built-in one exists

Before creating interface StringChecker { boolean check(String s); }, remember that Predicate<String> already does exactly that. Write your own only when a domain name like DiscountRule makes the code clearer.

Practice exercise

🏋️ Try it yourself

An airline app has seats as a record Seat(String code, boolean window, boolean taken): 1A (window, free), 1B (aisle, free), 2A (window, taken), 12C (aisle, taken), 14A (window, free). Build a Predicate<Seat> named freeWindow by combining a window predicate with the negation of a taken predicate. Use a Function<Seat, String> to turn each matching seat into text like Seat 1A (window) and print it.

Show solution
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;

public class FlightSeats {
    public static void main(String[] args) {
        List<Seat> seats = List.of(
                new Seat("1A", true, false),
                new Seat("1B", false, false),
                new Seat("2A", true, true),
                new Seat("12C", false, true),
                new Seat("14A", true, false));

        Predicate<Seat> window = Seat::window;
        Predicate<Seat> taken = Seat::taken;
        Predicate<Seat> freeWindow = window.and(taken.negate());
        Function<Seat, String> ticket = seat -> "Seat " + seat.code() + " (window)";

        for (Seat seat : seats) {
            if (freeWindow.test(seat)) {
                System.out.println(ticket.apply(seat));
            }
        }
    }
}

record Seat(String code, boolean window, boolean taken) {
}
Seat 1A (window)
Seat 14A (window)

What's next?

Predicates, functions and consumers are the building blocks of one of Java's most useful features. Next, put them to work on whole collections at once with Java streams.

Key takeaways

  • A functional interface has exactly one abstract method, so a lambda or method reference can implement it.
  • Default methods, static methods and methods from Object do not count toward the one-method limit.
  • @FunctionalInterface is optional, but it makes the compiler reject a second abstract method.
  • java.util.function provides ready-made interfaces: Predicate (test), Function (apply), Consumer (accept), Supplier (get), plus BiFunction, UnaryOperator and BinaryOperator.
  • Methods that accept functional interfaces as parameters can be reused with any rule the caller passes in.
  • Predicates combine with and, or and negate; functions chain with andThen (left to right) and compose (right to left).
  • Primitive versions like IntPredicate and IntBinaryOperator avoid boxing ints into Integer objects.

Frequently asked questions

What is a functional interface in Java?

It is an interface with exactly one abstract method. Because there is only one method to implement, you can create an instance with a lambda expression or a method reference instead of writing a class.

Is the @FunctionalInterface annotation required?

No. Any interface with one abstract method works with lambdas. The annotation is a safety check: the compiler reports an error if the interface ever gets a second abstract method.

Can a functional interface have default methods?

Yes. It can have any number of default and static methods, because they already have bodies. Only abstract methods count, and there must be exactly one.

What is the difference between Function and Predicate?

Both take one value. A Function<T, R> returns any type R through its apply method, while a Predicate<T> always returns a boolean through its test method and is used for yes or no checks.

What is the difference between Consumer and Supplier?

They are opposites. A Consumer<T> takes a value and returns nothing, so it is used for actions like printing. A Supplier<T> takes nothing and returns a value, so it is used to create or provide values on demand.