Skip to content
Chapter 17 · Core Java

Functional Interfaces and Lambda Expressions ​

The Problem: Too Much Boilerplate ​

Imagine you are a contractor hired to build one specific shelf in one specific room. When you arrive, you need to fill out paperwork, put on a badge, sign a liability waiver, get escorted to the room, and then spend about ten minutes actually building the shelf. The shelf is the point. Everything else is process.

That is what Java code looked like before Java 8 when you needed a quick, one off implementation of an interface. Consider a simple interface with one method:

java
interface Bird {
    void canFly(String value);
}

The traditional approach was to create a full named class:

java
// Way 1: named class
class Eagle implements Bird {
    @Override
    public void canFly(String value) {
        System.out.println("Eagle flying: " + value);
    }
}

// Then in main:
Bird eagle = new Eagle();
eagle.canFly("high");

This works fine if you need Eagle in multiple places. But what if you only need this implementation once, right here, and never again? Java gave you anonymous inner classes for exactly that situation:

java
// Way 2: anonymous inner class
Bird eagle = new Bird() {
    @Override
    public void canFly(String value) {
        System.out.println("Eagle flying: " + value);
    }
};

eagle.canFly("high");

Now count the lines that actually do useful work: one. The println line. Everything else is ceremony. You wrote new Bird(), opened a block, wrote @Override, typed public void canFly(String value), opened another block, closed both blocks, and added a semicolon. All of that just to wrap one line of logic.

When the interface has exactly one abstract method, the method name is completely redundant. There is only one method. The compiler already knows which method you are implementing. You are typing canFly for no reason other than Java syntax requiring it.

Java 8 introduced lambda expressions to solve exactly this problem. Lambda expressions let you drop all of that ceremony and write only the part that matters: the logic.

java
// Way 3: lambda expression (Java 8)
Bird eagle = (String value) -> System.out.println("Eagle flying: " + value);
eagle.canFly("high");

One line. Same result. The method name is gone. The @Override is gone. The new Bird() block is gone. But here is the critical question: why does this only work with interfaces that have exactly one abstract method? Because if there were two abstract methods, the compiler would not know which one your lambda is implementing. The single method is what makes this possible. This constraint is what a functional interface enforces.


What a Functional Interface Is ​

A functional interface is an interface that contains exactly one abstract method. That is the complete definition. Java also calls these SAM types, which stands for Single Abstract Method.

In an interface, every method signature you write without an implementation is automatically public abstract, even if you do not write those words. So any interface with exactly one such signature qualifies.

java
interface Bird {
    void canFly(String value);   // automatically public abstract
}

This is already a functional interface. You do not need to write anything special. The moment you have exactly one abstract method, you can use a lambda to implement it.

However, there is a trap here. Nothing stops you from coming back later and adding a second abstract method to Bird, at which point it silently stops being functional and every lambda targeting it breaks. To prevent this, Java 8 introduced the @FunctionalInterface annotation.


The @FunctionalInterface Annotation ​

Adding @FunctionalInterface above an interface does one thing: it tells the compiler to enforce the single abstract method constraint. If you or a teammate tries to add a second abstract method, the compiler immediately reports an error instead of silently breaking your lambdas.

java
@FunctionalInterface
interface Bird {
    void canFly(String value);   // the one and only abstract method
}

If you now try to add a second abstract method:

java
@FunctionalInterface
interface Bird {
    void canFly(String value);
    void canSwim();   // compile error: Unexpected @FunctionalInterface annotation
}

The annotation is optional. An interface with one abstract method is functional whether or not you annotate it. But using @FunctionalInterface is strongly recommended any time you intend an interface to be used with lambdas. It acts as documentation that signals your intent, and it gives you compiler protection for free.


What Does Not Count as an Abstract Method ​

A functional interface can have other members beyond its one abstract method. Default methods, static methods, and redeclarations of Object class methods do not count toward the limit. This confuses a lot of beginners, so let us look at each case.

Default methods have an implementation inside the interface. They are not abstract.

Static methods also have an implementation and cannot be overridden by implementing classes.

Object class methods are the interesting case. Every single class in Java implicitly extends Object. Object has methods like toString(), equals(), hashCode(), and clone(). If your interface declares a signature that matches one of those, it does not create a new abstract requirement because every implementing class already has an implementation from Object.

java
@FunctionalInterface
interface Bird {
    void canFly(String value);          // THE one abstract method

    default void getHeight() {           // allowed: has implementation
        System.out.println("height");
    }

    static void canEat() {              // allowed: static with implementation
        System.out.println("eating");
    }

    String toString();                  // allowed: Object already provides this
}

Even with all three of those extra members, Bird is still a functional interface because only canFly is abstract. You can implement Bird with a lambda, and the lambda will provide the implementation of canFly. The other methods are already handled.


Lambda Syntax Step by Step ​

Now that you understand what a functional interface is, let us see how lambda syntax is built up from scratch.

Start with the anonymous inner class version of implementing Bird:

java
Bird eagle = new Bird() {
    @Override
    public void canFly(String value) {
        System.out.println("Eagle flying: " + value);
    }
};

Step 1: Remove new Bird(), the opening block, @Override, and the method name. The compiler already knows the type from the left side and already knows the method from the interface definition.

java
Bird eagle = (String value) {
    System.out.println("Eagle flying: " + value);
};

Step 2: Add the lambda arrow -> between the parameter list and the body.

java
Bird eagle = (String value) -> {
    System.out.println("Eagle flying: " + value);
};

Step 3: If the body is a single statement, you can drop the curly braces entirely.

java
Bird eagle = (String value) -> System.out.println("Eagle flying: " + value);

Step 4: The compiler can infer the parameter type from the interface definition, so you can drop String:

java
Bird eagle = value -> System.out.println("Eagle flying: " + value);

This is a complete, working lambda expression. Each simplification is optional. You can write the full form with braces and explicit types whenever clarity benefits you. The minimal form removes everything the compiler can figure out on its own.

Lambda Syntax Rules ​

No parameters: Use empty parentheses.

java
Runnable r = () -> System.out.println("Running");

One parameter: Parentheses are optional, and the type is optional.

java
// all three are equivalent
Consumer<String> c = (String value) -> System.out.println(value);
Consumer<String> c = (value) -> System.out.println(value);
Consumer<String> c = value -> System.out.println(value);

Multiple parameters: Parentheses are required.

java
interface Adder { int add(int a, int b); }
Adder add = (a, b) -> a + b;

Single expression body: No braces, no return keyword. The expression result is automatically returned.

java
Function<Integer, Integer> doubler = n -> n * 2;

Multi statement body: Curly braces required, return keyword required.

java
Function<Integer, Integer> process = n -> {
    int doubled = n * 2;
    int shifted = doubled + 10;
    return shifted;
};

A lambda is not a standalone thing. It is always an implementation of a functional interface. The type on the left side determines which interface is being implemented, which determines which method signature the lambda must match.


The Four Built In Functional Interfaces ​

Because the same patterns come up constantly in real code, Java 8 includes a set of ready made functional interfaces in the java.util.function package. You do not need to write your own for the common cases. There are four main ones to know: Consumer, Supplier, Function, and Predicate.

Think of them by what they do with data: does your operation take data in, put data out, both, or just check a condition?

Consumer: Takes Input, Returns Nothing ​

A Consumer<T> represents an operation that accepts one value and does something with it but returns nothing. The abstract method is named accept.

You use Consumer when you want to process or log or store a value without producing a result.

java
import java.util.function.Consumer;

// Consumer that logs values greater than 10
Consumer<Integer> logging = value -> {
    if (value > 10) {
        System.out.println("Logging: " + value);
    }
};

logging.accept(11);   // prints "Logging: 11"
logging.accept(5);    // prints nothing, 5 is not greater than 10

Notice the call is logging.accept(11). The method name accept comes from the Consumer interface definition. You did not write accept in your lambda because the lambda only provides the body. The name is already fixed in the interface.

Supplier: Takes Nothing, Returns a Value ​

A Supplier<T> represents a source of values. It accepts no input and produces a result of type T. The abstract method is named get.

You use Supplier when you want to generate or fetch a value on demand without being given any input to start from.

java
import java.util.function.Supplier;

Supplier<String> quote = () -> "some random quote";
System.out.println(quote.get());   // "some random quote"

The parameter list is empty because Supplier takes no input. Each call to get() runs the lambda body and returns the result.

Function: Takes Input, Returns a Different Output ​

A Function<T, R> accepts one argument of type T and produces a result of type R. The abstract method is named apply. Use Function whenever you need to transform a value from one type to another.

java
import java.util.function.Function;

// Convert an integer to its string representation
Function<Integer, String> toText = num -> Integer.toString(num);
System.out.println(toText.apply(64));   // "64"

// Get the length of a string
Function<String, Integer> length = s -> s.length();
System.out.println(length.apply("hello"));   // 5

T is the input type. R is the return type. They can be the same type or different types.

Predicate: Takes Input, Returns Boolean ​

A Predicate<T> accepts one argument and returns a boolean. The abstract method is named test. Use Predicate whenever you are checking a condition: is this even? is this null? does this string start with a certain character?

java
import java.util.function.Predicate;

Predicate<Integer> isEven = num -> num % 2 == 0;
System.out.println(isEven.test(4));    // true
System.out.println(isEven.test(7));    // false

A Complete Side by Side Comparison ​

Here are all four functional interfaces together so you can see the pattern clearly:

java
import java.util.function.*;

public class FunctionalDemo {
    public static void main(String[] args) {

        // CONSUMER: in, no out
        Consumer<Integer> logger = value -> {
            if (value > 10) {
                System.out.println("Logger: " + value);
            }
        };
        logger.accept(15);   // "Logger: 15"
        logger.accept(3);    // nothing

        // SUPPLIER: no in, out
        Supplier<String> quoteSource = () -> "Think before you code";
        System.out.println(quoteSource.get());   // "Think before you code"

        // FUNCTION: in, out (can be different types)
        Function<Integer, String> intToString = num -> "Number: " + num;
        System.out.println(intToString.apply(42));   // "Number: 42"

        // PREDICATE: in, boolean out
        Predicate<Integer> isPositive = num -> num > 0;
        System.out.println(isPositive.test(5));    // true
        System.out.println(isPositive.test(-3));   // false
    }
}

The pattern is the same for all four: declare the reference with the functional interface type, assign a lambda that matches the expected method signature, then call the interface method by name (accept, get, apply, test).

If none of the four fits (for example you need four or five parameters), write your own functional interface and implement it with a lambda in the same way.


Inheritance and Functional Interfaces: Three Use Cases ​

Interfaces can extend other interfaces, which creates some interesting cases when functional interfaces are involved. These edge cases show up in interviews, so it is worth understanding them clearly.

Use Case 1: Functional Interface Extends a Non Functional Interface ​

If a functional interface tries to extend a nonfunctional interface that has its own abstract method, the child inherits that abstract method. Now the child has two abstract methods, which violates the functional interface constraint.

java
interface LivingThing {
    void canBreathe();   // abstract method 1
}

// WRONG: This causes a compile error with @FunctionalInterface
// @FunctionalInterface
// interface Bird extends LivingThing {
//     void canFly(String value);   // abstract method 2 -- total: 2, not allowed
// }

The fix is to make the parent's method a default method instead of an abstract one:

java
interface LivingThing {
    default void canBreathe() {   // default: has an implementation, not abstract
        System.out.println("breathing...");
    }
}

@FunctionalInterface
interface Bird extends LivingThing {
    void canFly(String value);   // still the only abstract method
}

Now Bird is a valid functional interface because canBreathe has a default implementation and does not count as abstract.

Use Case 2: Non Functional Interface Extends a Functional Interface ​

A regular interface (without @FunctionalInterface) can extend a functional interface and add its own abstract methods. The result has two abstract methods and is no longer functional, but this is perfectly legal for a regular interface.

java
@FunctionalInterface
interface LivingThing {
    void canBreathe();   // functional: one abstract method
}

// Not annotated as @FunctionalInterface, so adding more abstracts is allowed
interface Bird extends LivingThing {
    void canFly(String value);   // inherits canBreathe + adds canFly = two abstracts, fine
}

Bird now has two abstract methods, so you cannot use a lambda to implement it. You would need an anonymous class or a named class that implements both. But the code compiles fine because Bird is just a normal interface with two abstract methods.

Use Case 3: Functional Interface Extends Another Functional Interface ​

This is the tricky case. If both the parent and child are annotated as @FunctionalInterface and both declare an abstract method, that is two abstract methods total, which causes a compile error.

java
@FunctionalInterface
interface LivingThing {
    boolean canBreathe();
}

// WRONG: two different abstract methods total
// @FunctionalInterface
// interface Bird extends LivingThing {
//     void canFly(String value);   // different signature: total of 2 abstracts -- error
// }

But there is one situation where two functional interfaces can have a legal parent child relationship: when both declare a method with the exact same signature. In that case, the child's declaration is treated as an override and counts as the single abstract method.

java
@FunctionalInterface
interface LivingThing {
    boolean canBreathe();
}

@FunctionalInterface
interface Bird extends LivingThing {
    boolean canBreathe();   // same signature: override, still one abstract method total
}

// Using it with a lambda:
Bird eagle = () -> true;   // implements canBreathe, returns true
System.out.println(eagle.canBreathe());   // true

Both interfaces declare boolean canBreathe(). The child simply reaffirms the same contract. The compiler counts it as one abstract method, and lambdas work normally.


Why Lambda Expression Only Works with Functional Interfaces ​

This question comes up in every interview on this topic, so make sure you can answer it clearly and completely.

Lambda expressions work only with functional interfaces because the single abstract method is what allows the compiler to connect the lambda to the right method. When you write:

java
Bird eagle = value -> System.out.println("Eagle flying: " + value);

The compiler looks at the left side, sees the type is Bird, looks up Bird's single abstract method canFly(String value), and maps the lambda to that method. The lambda's parameter list and return type must match canFly's signature.

If Bird had two abstract methods, the compiler would not know which method the lambda is implementing. Should it map to canFly or to some other method? There is no way to know. That is why a lambda requires exactly one abstract method: it is the unambiguous target.

This is also why you cannot use a lambda with a regular class or a regular interface that has multiple abstract methods. Lambdas exist specifically to eliminate the boilerplate of anonymous inner classes for the SAM pattern, and that pattern only makes sense when there is exactly one method to implement.


Check yourself ​

  1. What is a functional interface, and what is it also called?
  2. Is @FunctionalInterface required? What does it add?
  3. Which methods do not count toward the one abstract method?
  4. Name three ways to implement a functional interface. What does the lambda remove?
  5. When can you drop the braces in a lambda body?
  6. What do Consumer, Supplier, Function and Predicate take and return, and what is each method called?
  7. A @FunctionalInterface extends an interface that has an abstract method and adds its own. What happens? When is it allowed?
Answers
  1. An interface with exactly one abstract method; also called a SAM (Single Abstract Method) interface.
  2. No, it is optional. With it, the compiler rejects a second abstract method.
  3. Default methods, static methods, and methods that Object already has (such as toString()).
  4. A class with implements, an anonymous class, and a lambda. The lambda removes new, the method name, @Override and the extra braces: only parameters, ->, and the body remain.
  5. When the body is a single line (then return is also not written).
  6. Consumer<T>: one input, no result, accept. Supplier<T>: no input, a result, get. Function<T, R>: one input, a result, apply. Predicate<T>: one input, a boolean, test.
  7. Compile error: it now has two abstract methods. It is allowed if the parent's method is a default method, or if both declare the exact same method signature.