Lambda Expressions
Write short, inline implementations of one-method interfaces for sorting, callbacks and request handlers, and avoid the capture and exception traps.
(Type a, Type b) -> expression
param -> expression
(a, b) -> { statements; return value; }
() -> expressionA 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
ifails this; copy it into a fresh local inside the loop. If you need to accumulate, use the return value, a collection, or anAtomicInteger, not a reassigned local. - Checked exceptions. The lambda can only throw the checked exceptions its interface method declares.
Function.applydeclares none, so a lambda that calls a method throwingIOExceptionmust catch it, usually wrapping it inUncheckedIOException.HttpHandler.handledeclaresIOException, so there it is allowed. - Expecting
thisto mean the lambda. Inside a lambda,thisis 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 capturesthis, 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
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.
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 3A 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.
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.
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