Lambda Expressions

Write short, inline implementations of one-method interfaces for sorting, callbacks and request handlers, and avoid the capture and exception traps.

Syntax(Type a, Type b) -> expression param -> expression (a, b) -> { statements; return value; } () -> expression

A lambda expression is a compact way to write an implementation of an interface that has exactly one abstract method, a functional interface. Instead of declaring a class, implementing the method and creating an instance, you write the parameters, an arrow and the body. The compiler works out which interface you mean from where the lambda is used: its target type.

The forms

Runnable job = () -> IO.println("tick");            // no parameters
Predicate<String> blank = s -> s.isBlank();          // one parameter, no brackets needed
Comparator<User> byAge = (a, b) -> Integer.compare(a.age(), b.age());
Function<String, Integer> size = s -> {              // block body needs return
    String t = s.strip();
    return t.length();
};

Parameter types are usually inferred. You can write them out, or use var (Java 11) when you want to put an annotation on a parameter. Since Java 22 an unused parameter can be named _, as in map.forEach((_, value) -> ...).

Where you meet them in back-end code

Sorting with a Comparator, filtering with removeIf, tasks handed to an ExecutorService, retry and transaction callbacks, and request handlers. The JDK's own HttpServer takes an HttpHandler, which is a functional interface, so a whole endpoint can be a lambda. Frameworks work the same way: a route, a filter or an event listener is often just a lambda you pass in.

Mistakes people make

  • Changing a captured variable. A lambda may read local variables from the enclosing method only if they are effectively final: assigned once and never changed. A loop counter i fails this; copy it into a fresh local inside the loop. If you need to accumulate, use the return value, a collection, or an AtomicInteger, not a reassigned local.
  • Checked exceptions. The lambda can only throw the checked exceptions its interface method declares. Function.apply declares none, so a lambda that calls a method throwing IOException must catch it, usually wrapping it in UncheckedIOException. HttpHandler.handle declares IOException, so there it is allowed.
  • Expecting this to mean the lambda. Inside a lambda, this is the enclosing object, unlike an anonymous class where it is the anonymous instance. That is usually what you want, but a lambda that uses a field or an instance method captures this, so it keeps the whole object reachable for as long as the lambda lives, for example in a cache or a long-lived listener list.
  • Thirty-line lambdas. Once the body needs a comment to explain it, move it to a named method and pass a method reference. Stack traces and tests get easier too.

A lambda has no state of its own and no name, which is why it suits small behaviour passed to someone else. Anything with fields, several methods or a lifecycle still belongs in a class.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Sort search results by relevance and then by date with a Comparator lambda before returning a page of them.
  • Register small endpoints such as /health on the JDK HttpServer without writing a handler class for each.
  • Pass retry or transaction callbacks to a helper that opens the connection, runs the lambda and commits or rolls back.

More examples

Capturing a loop value safely

Each iteration declares its own jobId, so every lambda captures a different, unchanging value. Capturing the loop counter itself is rejected by the compiler.

Example · java
void main() {
    List<Runnable> jobs = new ArrayList<>();
    for (int i = 1; i <= 3; i++) {
        int jobId = i; // a new effectively final local on every iteration
        jobs.add(() -> IO.println("running job " + jobId));
        // jobs.add(() -> IO.println(i)); does not compile: i keeps changing
    }
    for (Runnable job : jobs) {
        job.run();
    }
}
// Output:
// running job 1
// running job 2
// running job 3

A whole endpoint as a lambda

Because HttpHandler is a functional interface, the handler needs no class of its own. Its method declares IOException, so the lambda can call write without a try/catch for it.

Example · java
import com.sun.net.httpserver.HttpServer;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

void main() throws Exception {
    HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
    // HttpHandler has one abstract method, handle(HttpExchange), which may throw IOException.
    server.createContext("/health", exchange -> {
        byte[] body = "{\"status\":\"up\"}".getBytes(StandardCharsets.UTF_8);
        exchange.getResponseHeaders().set("Content-Type", "application/json");
        exchange.sendResponseHeaders(200, body.length);
        try (var out = exchange.getResponseBody()) {
            out.write(body);
        }
    });
    server.start();
    try (HttpClient client = HttpClient.newHttpClient()) {
        URI uri = URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/health");
        HttpResponse<String> res = client.send(HttpRequest.newBuilder(uri).build(),
                HttpResponse.BodyHandlers.ofString());
        IO.println(res.statusCode() + " " + res.body());
    } finally {
        server.stop(0);
    }
}
// Output:
// 200 {"status":"up"}

Checked exceptions inside a Function

Wrapping in UncheckedIOException keeps the original exception as the cause, so nothing is lost. The caller unwraps it with getCause() where it can actually handle the failure.

Example · java
String loadTemplate(String name) throws IOException {
    if (name.startsWith("missing")) {
        throw new IOException("no template " + name);
    }
    return "<h1>" + name + "</h1>";
}

void main() {
    // Function.apply declares no checked exceptions, so the lambda must deal with IOException.
    Function<String, String> load = name -> {
        try {
            return loadTemplate(name);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    };
    for (String name : List.of("welcome", "missing-reset")) {
        try {
            IO.println(load.apply(name));
        } catch (UncheckedIOException e) {
            IO.println("failed: " + e.getCause().getMessage());
        }
    }
}
// Output:
// <h1>welcome</h1>
// failed: no template missing-reset

Discussion

  • Be the first to comment on this lesson.