Course contents

Functional interfaces in Java

The single-abstract-method rule and @FunctionalInterface, the java.util.function families and primitive specialisations, composition, the four kinds of method reference, how lambdas compile through invokedynamic, and checked exceptions in lambdas.

PremiumMust know for interviewsLesson16 min readCoreReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: one socket, many plugs
  2. The one-abstract-method rule under the hood
  3. Real world: checked exceptions and your own interfaces
  4. Advanced: overloads, this, and what interviewers poke at
  5. How to say it in the interview

🎯 What is a functional interface?

A functional interface is an interface with exactly one abstract method. That single method is the "shape" a lambda or method reference fills in. Predicate<Order> has one abstract method, boolean test(Order o), so o -> o.total() > 500 is a complete implementation of it, no class required.

Lambdas arrived in Java 8, and the trick that made them fit into a 20-year-old type system is the functional interface. Java didn't add a "function type" like (Order) -> boolean. It reused interfaces, and said: if an interface has one abstract method, a lambda can stand in for it. Interviewers like this topic because it sits right on the line between syntax and the JVM. Anyone can write x -> x * 2; fewer people can tell you which interface it became, what javac generated, or why forEach won't let them throw an IOException.

What you’ll learn

  • What makes an interface functional, and why default, static and Object methods don't count towards the one abstract method
  • The java.util.function families (Function, Supplier, Consumer, Predicate, operators) and when to reach for IntPredicate or ToLongFunction to avoid boxing
  • How to compose behaviour with andThen, compose, Predicate.and and Predicate.not
  • The four kinds of method reference and the eager-evaluation trap with bound receivers
  • How a lambda compiles: a private lambda$ method, invokedynamic, LambdaMetafactory and a hidden class, with non-capturing lambdas reused as singletons
  • Practical options for checked exceptions inside lambdas, and how to design your own functional interface

For this lesson, we'll focus on the single-abstract-method rule, the java.util.function toolbox, composition, method references, how lambdas compile, checked exceptions, and how to design your own functional interface.

Below the line (out of scope)

The 30-second answer

A functional interface has exactly one abstract method; default methods, static methods and re-declared Object methods like equals don't count. @FunctionalInterface is optional, but it makes the compiler enforce the rule. java.util.function gives you 43 ready-made shapes: Function, BiFunction, Supplier, Consumer, Predicate, UnaryOperator, BinaryOperator, plus primitive versions like IntPredicate that avoid boxing. A lambda compiles to a private method plus an invokedynamic call site; on first use LambdaMetafactory spins a hidden class, so there's no $1.class file, and a non-capturing lambda is created once and reused.

Interview radar

Asked very often: What makes an interface functional, what does @FunctionalInterface do, and can you name the core java.util.function types? Senior rounds probe: How a lambda compiles to invokedynamic and LambdaMetafactory, why lambdas can't throw checked exceptions through Function, and when primitive specialisations matter. Production angle: A hot path using Function<Integer, Integer> that boxes every value and shows up as allocation pressure in a profiler.

01Picture it first: one socket, many plugs

Think of a functional interface as a wall socket with one specific shape. The socket doesn't care what you plug in, a lamp, a charger, a kettle, as long as the plug matches. Predicate<Order> is a socket shaped "takes an Order, returns a boolean". A lambda, a method reference or a full class can all be plugs, provided their shape fits.

That also explains why the same lambda text can become different types. s -> s.isEmpty() fits Predicate<String> and also Function<String, Boolean>. The lambda has no type of its own; it takes the type of the socket it's plugged into, which the JLS calls the target type.

The standard library ships a whole wall of sockets, grouped by shape. Here's the map I keep in my head:

Fig. 1

A map of the java.util.function families grouped by shape: Function and BiFunction transform, Supplier produces, Consumer accepts, Predicate tests, UnaryOperator and BinaryOperator keep the same type, each with primitive variants such as IntPredicate and ToLongFunction

A map of the java.util.function families grouped by shape: Function and BiFunction transform, Supplier produces, Consumer accepts, Predicate tests, UnaryOperator and BinaryOperator keep the same type, each with primitive variants such as IntPredicate and ToLongFunction

Notice the naming pattern, because it lets you guess an interface you've never used. Bi means two inputs. A primitive prefix like Int means the input is primitive (IntPredicate.test(int)). To plus a primitive means the output is primitive (ToIntFunction.applyAsInt(T)). Obj plus a primitive mixes an object and a primitive (ObjIntConsumer.accept(T, int)).

02The one-abstract-method rule under the hood

Try it yourself first

Before reading on: which of these three interfaces are functional? Count abstract methods carefully.

@FunctionalInterface
interface PriceRule {
    long apply(long cents);                      // abstract
    default PriceRule then(PriceRule next) {     // default: doesn't count
        return c -> next.apply(apply(c));
    }
    static PriceRule identity() { return c -> c; } // static: doesn't count
    boolean equals(Object o);                    // Object method: doesn't count
    String toString();                           // Object method: doesn't count
}

interface Auditable {
    void audit();
    void flush();                                // two abstract methods
}

interface RefundRule extends PriceRule {}        // inherits exactly one

PriceRule is functional: one abstract method, apply. Auditable is not, so Auditable a = () -> {} fails with "incompatible types: Auditable is not a functional interface". RefundRule is functional, because it inherits one abstract method and adds none.

Here's the count the compiler does for each of the three, member by member.

Fig. 2

PriceRule's members counted one by one: apply is abstract and counts, while the default, static and re-declared Object methods don't, so the total is one and it is functional; Auditable has two abstract methods and is not functional; RefundRule inherits one and is functional

PriceRule's members counted one by one: apply is abstract and counts, while the default, static and re-declared Object methods don't, so the total is one and it is functional; Auditable has two abstract methods and is not functional; RefundRule inherits one and is functional

Notice three things:

  • default and static methods have bodies, so they aren't abstract, and a lambda doesn't need to supply them. This is how Function can have andThen and still be a lambda target.
  • Re-declaring an Object method doesn't add an abstract method. Every implementation already inherits equals and toString from Object. That's why Comparator declares boolean equals(Object) and is still functional (JLS §9.8).
  • @FunctionalInterface changes nothing at runtime. It's a compile-time check plus documentation. Without it, Runnable-style interfaces still work with lambdas; with it, the next person who adds a second abstract method gets a compile error instead of breaking every caller.

Generic methods are the one exception you might hit: an interface whose single abstract method is itself generic (<T> T convert(String s)) is functional, but a lambda can't implement it, because lambdas can't declare type parameters. Only a method reference can.

The families in practice

Try it yourself first

Before reading on, pick the right interface for each: "is this order eligible for free shipping?", "load the config lazily", "send this receipt email", "convert an Order to an OrderDto".

Predicate<Order>          freeShipping = o -> o.totalCents() >= 50_000;
Supplier<Config>          config       = () -> Config.loadFrom("app.yaml");
Consumer<Receipt>         sendEmail    = r -> mailer.send(r.customerEmail(), r.render());
Function<Order, OrderDto> toDto        = o -> new OrderDto(o.id(), o.totalCents());

BiFunction<Long, Integer, Long> discount  = (cents, pct) -> cents * (100 - pct) / 100;
UnaryOperator<String>           normalise = s -> s.trim().toLowerCase();  // T -> T
BinaryOperator<Long>            add       = Long::sum;                    // (T, T) -> T

ToLongFunction<Order> totalCents = Order::totalCents;   // returns long, no Long box
IntPredicate          isEven     = n -> n % 2 == 0;     // takes int, no Integer box

UnaryOperator<T> extends Function<T, T> and BinaryOperator<T> extends BiFunction<T, T, T>; they exist to make "same type in and out" readable. There's no TriFunction. If you need three inputs, either group them into a record or write your own interface.

Premium lesson

Keep reading with the Java Pass

The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.

  • Real world: checked exceptions and your own interfaces
  • Advanced: overloads, this, and what interviewers poke at
  • How to say it in the interview

From ₹199 · one-time payment · No auto-renewal

Practise this topicFunctional Java basics: interview questions & scenarios
Further reading (10)