Lambda Expressions ·
‹ Previous Next ›
⏱ 5 min read Modified: 2026-09-07

Functional Interface in Java

The interface java.util.Comparator declares two abstract methods: compare(T, T) and equals(Object). Yet the line Comparator<Car> byCost = (a, b) -> a.getCostUSD() - b.getCostUSD(); compiles without a complaint, and the @FunctionalInterface annotation sits on Comparator perfectly legally. Understanding why that second abstract method does not count is the key to this whole topic.

A functional interface in Java is an interface with exactly one abstract method. Such an interface can serve as the target type of a lambda expression or a method reference. The number of default, static and private methods is not limited.

1. What is a functional interface

A lambda expression does not exist on its own: it always needs a type. That type is a functional interface — an interface with a single abstract method, also called a SAM interface (Single Abstract Method). The single abstract method itself is called the functional method.

Example 1. Declaring a functional interface

@FunctionalInterface
public interface SomeInterface {
    void doSomething();
}

Now the interface can be implemented by a lambda expression instead of a class:

SomeInterface task = () -> System.out.println("done");
task.doSomething(); // prints: done

The formal requirements are:

  1. Exactly one abstract method.
  2. Any number of default, static, private and private static methods (the last two are allowed since Java 9).
  3. Abstract declarations that repeat the public methods of java.lang.Object (equals, hashCode, toString) are not counted.

An interface with a default method is still functional:

interface A {
    default int defaultMethod() {
        return 0;
    }
    void method(); // the only abstract method
}

There may be as many non-abstract methods as you like:

interface B {
    default int defaultMethod() {
        return 0;
    }
    static B empty() {
        return () -> {};
    }
    private int helper() { // Java 9+
        return 42;
    }
    void method();
}

And here is the Object case from the opening — the interface is functional, because only method() counts as abstract:

interface C {
    boolean equals(Object o);
    int hashCode();
    String toString();
    void method(); // effectively the only abstract method
}

Why equals does not count

Every class that implements an interface already inherits equals, hashCode and toString from Object. A lambda would have nothing to implement there, so the compiler excludes such declarations when counting abstract methods. That is exactly why Comparator, with both compare and equals, remains a functional interface.

The JDK is full of interfaces with a single abstract method: java.lang.Runnable, java.util.concurrent.Callable, java.util.Comparator, java.awt.event.ActionListener, java.lang.Iterable. They did not change in Java 8 — the language did. Since Java 8 any such interface can be the target type of a lambda expression, and most of them were annotated with @FunctionalInterface to lock that contract in. Iterable, for instance, gained two default methods (forEach and spliterator), but its only abstract method is still iterator(), so it stays functional. Always judge an interface by the number of abstract methods, not by the number of declared ones.

2. Function descriptor

The term function descriptor describes the signature of the functional method — and therefore the shape of any lambda expression that can implement it. The notation is parameters -> return type: parameter types on the left of the arrow, the result type on the right. The method name is not part of the descriptor.

SomeInterface from Example 1 has the method doSomething(), which takes no parameters and returns void. Its function descriptor is () -> void.

Example 2. One parameter, a value returned

@FunctionalInterface
public interface SomeInterface1 {
    int someMethod1(String param);
}

The function descriptor is String -> int, so the lambda must accept a string and return an int:

SomeInterface1 length = s -> s.length();

Example 3. Two parameters, no result

@FunctionalInterface
public interface SomeInterface2 {
    void someMethod2(int a1, int a2);
}

The function descriptor is (int, int) -> void.

Example 4. Function descriptor of the Consumer interface

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
    // plus default methods, for example andThen(Consumer<? super T> after)
}

The function descriptor of Consumer is T -> void.

Functional method Function descriptor Matching lambda
void doSomething() () -> void () -> System.out.println("hi")
int someMethod1(String param) String -> int s -> s.length()
void someMethod2(int a1, int a2) (int, int) -> void (a, b) -> System.out.println(a + b)
void accept(T t) T -> void car -> System.out.println(car)

3. Target type: how a lambda learns its interface

A lambda expression carries no information about the interface it implements. The compiler infers that type from the context in which the lambda appears, and this context type is called the target type.

The target type comes from:

  • a variable declaration (Searchable s = c -> ...;);
  • a method parameter (list.removeIf(c -> ...));
  • a method return statement;
  • a cast ((Searchable) c -> ...);
  • a ternary expression or an array initializer.

Consequently, the same lambda body fits different interfaces as long as the descriptors of their functional methods match:

interface Searchable {
    boolean test(Car car);
}
interface Saleable {
    boolean approve(Car car);
}
//...
Searchable s1 = c -> c.getCostUSD() > 20000;
Saleable   s2 = c -> c.getCostUSD() > 20000;

Note that the method names (test and approve) differ, and it does not matter. Only the parameter list, the return type and the declared exceptions are compared.

For the same reason, code without a target type does not compile:

// Compile error: the target type cannot be inferred
var f = () -> System.out.println("hi");

// Compile error: Object is not a functional interface
Object o = c -> c.getCostUSD() > 20000;

4. Which methods are allowed in a functional interface

Kind of method How many are allowed Counted as abstract Available since
Abstract Exactly one Yes Java 1.0
default Any number No Java 8
static Any number No Java 8
private / private static Any number No Java 9
Abstract declaration of a public Object method Any number No Java 8

5. The @FunctionalInterface annotation

Java 8 introduced the @FunctionalInterface annotation. It changes nothing at runtime, but it makes the compiler verify that the interface really has exactly one abstract method. If it does not, the build fails.

// Does not compile: two abstract methods
@FunctionalInterface
interface A {
    void m(int i);
    void m(long l);
}

The annotation is optional: an interface with a single abstract method is functional without it, and a lambda can be assigned to it anyway. Still, it is worth adding — it protects the interface from someone quietly adding a second abstract method and breaking every lambda that relied on it.

Worth knowing

@FunctionalInterface can be placed on an interface only — not on a class, an enum or an annotation type. It is not inherited either: a sub-interface has to be annotated again if you want the same compile-time check.

6. Built-in functional interfaces of java.util.function

You rarely need to declare your own interface for every lambda: Java 8 added the java.util.function package with ready-made interfaces for the common signatures.

Interface Abstract method What it does Example lambda
Predicate<T> boolean test(T t) Checks a condition c -> c.getCostUSD() > 20000
Consumer<T> void accept(T t) Takes a value and returns nothing c -> System.out.println(c)
Function<T, R> R apply(T t) Converts one value into another c -> c.getModel()
Supplier<T> T get() Supplies a value, takes no arguments () -> new Car()
UnaryOperator<T> T apply(T t) A Function whose argument and result share one type s -> s.trim()
BinaryOperator<T> T apply(T a, T b) Folds two values of one type into one (a, b) -> a + b
BiFunction<T, U, R> R apply(T t, U u) A function of two arguments (c, k) -> c.getCostUSD() * k
BiPredicate<T, U> boolean test(T t, U u) A condition over two arguments (c, max) -> c.getCostUSD() < max
BiConsumer<T, U> void accept(T t, U u) An action over a pair of values (k, v) -> map.put(k, v)

For primitives there are specializations that avoid boxing: IntPredicate, IntFunction<R>, ToIntFunction<T>, IntUnaryOperator, IntBinaryOperator, IntSupplier, IntConsumer and their long and double counterparts. They pay off in code that runs often.

Functional interfaces also live outside this package: Runnable (void run()), Callable<V> (V call()), Comparator<T> (int compare(T, T)), Iterable<T> and AutoCloseable.

7. Common misconceptions

  1. "A functional interface is an interface with one method." Not one method — one abstract method. There may be any number of default, static and private methods.
  2. "Without @FunctionalInterface you cannot use a lambda." You can: the annotation only enables a compiler check. Runnable worked fine with lambdas before it was annotated.
  3. "A lambda is shorthand for an anonymous class." No: different this, different scoping rules and a fundamentally different compilation strategy (see the table above).
  4. "The lambda must somehow match the method name." The compiler compares the signature only: parameters, return type and throws. The name of the abstract method plays no role.

Frequently asked questions

Is the @FunctionalInterface annotation required?

No. Any interface with exactly one abstract method is functional and can be the target type of a lambda even without the annotation. The annotation acts as a contract: it makes the compiler report an error if someone adds a second abstract method later.

Can a lambda implement a generic method?

No. If the abstract method declares its own type parameters, for example a method convert that declares the type parameter T, a lambda expression will not compile for it, because a lambda cannot declare type parameters. Use an anonymous class or a method reference instead.

Does an interface stay functional when it extends another interface?

Yes, as long as the total number of abstract methods is still one. A sub-interface that only overrides the inherited method or adds default methods remains functional. However, you have to repeat the @FunctionalInterface annotation, because it is not inherited.

Why can a lambda not modify a local variable?

A lambda captures a copy of the value, not the variable itself, so the variable must be final or effectively final. A change made inside the lambda would not be visible outside, and the compiler rejects such code. To accumulate a result, use a field, a one-element array or an AtomicInteger.

Are "SAM interface" and "functional interface" the same thing?

Practically yes. SAM stands for Single Abstract Method and describes the shape of the interface; "functional interface" is the term used by the Java Language Specification for exactly the same idea. SAM conversion is the older name for turning a lambda or a method reference into an instance of such an interface.

Comments

Please log in or register to have a possibility to add comment.