Skip to content

VOL 01 / CH 08 / LESSON 06

8.6 Lambda Expressions and Functional Interfaces ​

Prerequisites: Methods from Chapter 5, Interfaces from Chapter 8

Estimated time: 25 minutes

Runtime environment: Java 17

You are ready to continue when: You can declare a functional interface, write a lambda, and explain its target type and captured variables

An alert system handles three events: low disk space notifies the on-call engineer, a completed job gets logged, and a lost connection prompts a retry. A separate EventHandler class for each event works, but each class is used once and only a few lines inside handle differ.

Lambda solves exactly this problem: when a specific interface implementation is needed, simply provide that segment of behavior directly.

1. Functional Interfaces Specify Inputs and Results ​

Lambdas cannot exist in isolation from their type. They require a single abstract method interface as the target type:

java
@FunctionalInterface
interface EventHandler {
    void handle(Event event);
}

@FunctionalInterface is not required syntax, but it's worth keeping. If someone later adds a second abstract method, the compiler will immediately point out that the interface can no longer hold the Lambda.

"One abstract method" should be counted according to the interface contract:

  • default, static, and private are not abstract methods;
  • Declarations with the same method signature as Object's public are not counted;
  • Abstract methods inherited from parent interfaces should also be considered;
  • If two declarations can be merged into a single method contract, the interface may still be a functional interface.
java
@FunctionalInterface
interface NamedTask {
    void run();

    default String label() { return "Unnamed task"; }
    static NamedTask idle() { return () -> { }; }
    boolean equals(Object other); // Matches an Object public method; does not add a functional method
}

Runnable, Callable<T>, and Comparator<T> are all functional interfaces in the standard library. The Lambda ultimately implements the functional method within one of them.

2. From Anonymous Classes to Lambdas ​

The anonymous class approach emphasizes "creating an object and implementing a method":

java
EventHandler handler = new EventHandler() {
    @Override
    public void handle(Event event) {
        System.out.println("Event:" + event.name());
    }
};

The lambda approach emphasizes "what to do after receiving parameters":

java
EventHandler handler = event -> System.out.println("Event:" + event.name());

These snippets need the relevant imports from java.util.function, plus java.util.Comparator. The runnable example below defines Event; try snippets with repeated variable names separately. Common forms include:

java
Runnable start = () -> System.out.println("Start task");
Consumer<String> print = message -> System.out.println(message);
Comparator<Integer> order = (left, right) -> Integer.compare(left, right);
Function<String, Integer> length = text -> {
    String clean = text.strip();
    return clean.length();
};

Here, Integer.compare is used instead of left - right. Subtraction may overflow on extreme integers, leading to incorrect ordering.

The syntax can be read as:

text
(parameter list) -> expression
(parameter list) -> { statements; return result; }
  • When there are no parameters, () must be written;
  • When there is only one parameter inferred to a type, the parentheses can be omitted;
  • Multiple parameters must be enclosed in parentheses;
  • For a value-returning target method, the expression supplies the result without return. A void target can use a statement expression such as a method call: () -> System.out.println("start");
  • If a statement block requires a return value, all normal exit paths must explicitly use return.

3. Target Type Determines What Lambda Is ​

Looking at the Lambda alone doesn't give a complete type:

java
text -> text.length()

A compiler can only determine the parameter and return types after assigning, passing, or explicitly casting to its target type:

java
Function<String, Integer> length = text -> text.length();
ToIntFunction<String> fastLength = text -> text.length();

A Lambda with the same shape may match multiple overloads, causing ambiguity:

java
static void use(Function<String, Integer> function) { }
static void use(ToIntFunction<String> function) { }

// use(text -> text.length()); // Compilation error: Two overloads could match
use((ToIntFunction<String>) text -> text.length());

In engineering code, it's better to use meaningful variables or different method names to eliminate ambiguity; casting is suitable for explaining rules and shouldn't be used everywhere.

4. Four Common Standard Interfaces ​

They are all in the java.util.function package:

InterfaceFunction MethodQuestion TypeExample
Supplier<T>T get()What is produced() -> "system is ready"
Consumer<T>void accept(T)How is it consumedSystem.out::println
Function<T, R>R apply(T)How is it convertedString::length
Predicate<T>boolean test(T)Does it meettext -> !text.isBlank()

There are also combinations such as UnaryOperator<T>, BinaryOperator<T>, and BiFunction<T,U,R>, as well as primitive type specializations such as IntPredicate and ToIntFunction<T>. When handling a large number of int, specialized interfaces can avoid repeatedly boxing them into Integer.

5. Capturing Local Values, Limiting Local Variables ​

A lambda can read local variables from the enclosing method, but the variable must be either final or effectively final: assigned once and not reassigned afterward.

java
String prefix = "[Message]";
EventHandler handler = event ->
        System.out.println(prefix + " " + event.name());

// prefix = "[Emergency]"; // If uncommented, prefix is no longer effectively final

This is the language rule Java uses for capturing local variables. Do not interpret it simply as "Lambdas always run in another thread": Lambdas can run synchronously, and the rule still holds.

Note that the inability to reassign a local variable does not mean the captured object is immutable:

java
List<String> audit = new ArrayList<>();
EventHandler handler = event -> audit.add(event.name()); // The reference didn’t change, the object content did.

This code compiles, but sharing mutable objects increases the difficulty of reasoning, and in concurrent environments, it can lead to race conditions. Prefer to have Lambdas return results or delegate state management to explicit objects; do not use single-element arrays as a default trick to bypass the rule.

The this in a lambda also follows the lexical scope of the outer layer, pointing to the enclosing object; anonymous inner classes, on the other hand, have their own this. The two are not just different by a few lines of syntax.

6. A Runnable Alert Example ​

Save this as LambdaDemo.java. The declaration record Event(String name, int level) is a compact data class: Java generates a constructor and the name() and level() accessors, and its component fields are final. Records do not automatically validate nulls or numeric ranges; this example uses fixed valid inputs.

java
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

public class LambdaDemo {
    public static void main(String[] args) {
        List<Event> events = List.of(
                new Event("Low disk space", 5),
                new Event("Job completed", 1),
                new Event("Connection lost", 4));

        Predicate<Event> urgent = event -> event.level() >= 4;
        Function<Event, String> format = event ->
                "[" + event.level() + "] " + event.name();
        Consumer<String> announce = System.out::println;
        Supplier<String> ready = () -> "Alarm system ready";

        System.out.println(ready.get());
        for (Event event : events) {
            if (urgent.test(event)) {
                announce.accept(format.apply(event));
            }
        }

        EventHandler handler = event ->
                System.out.println("Logged: " + event.name());
        handler.handle(events.get(0));
    }

    @FunctionalInterface
    interface EventHandler {
        void handle(Event event);
    }

    record Event(String name, int level) { }
}

Compile and run:

bash
javac --release 17 -encoding UTF-8 LambdaDemo.java
java LambdaDemo

Expected output:

text
Alarm system ready
[5] Low disk space
[4] Connection lost
Logged: Low disk space

7. Exceptions and Limits ​

Checked Exceptions Don't Just Disappear ​

Whether a lambda can throw a checked exception depends on the method signature of the target interface. Runnable.run() declares no checked exceptions, so its lambda cannot directly let an IOException escape. Either handle it immediately or use an interface that properly declares the appropriate exception. Don't use a broad catch (Exception) to hide failures.

Lambda Isn't a Substitute for All Small Classes ​

One-time, stateless operations are well suited for lambdas. If the behavior has a clear domain name, multiple related operations, or requires independent state and complex testing, naming a class is usually clearer. The goal is to make the intent more direct, not to turn all code into one line.

8. Language Boundaries ​

  • Python's lambda can only contain a single expression; multi-line logic is typically written using def; Python does not require a functional interface like Java.
  • C++ Lambda expressions explicitly select value capture, reference capture, or this using a capture list; Java does not have an equivalent syntax for reference capture.
  • Java 17 is the runnable baseline for this chapter; other languages are used for identifying differences and cannot be translated word-for-word.

Before Continuing ​

Give a Lambda two possible target types and observe when explicit typing is needed to resolve overload ambiguity. Then have it capture a local variable: reassigning the variable should cause a compilation failure, while modifying the mutable object it refers to may compile successfully. The latter is not a recommended approach, illustrating that the effectively final constraint applies to variable binding. Integer comparators use Integer.compare, do not use subtraction that might overflow.

→ Next: Stream API Collection Pipeline

Normative Entry: JLS §15.27: Lambda Expressions.

Built with VitePress | Software Systems Atlas