- Prerequisites
- 1.1 Functional Programming Concepts
- Covers
- Parameters · Expression body · Statement body
- Related
- 1.3 Variable Capture · 2.3 Creating Functional Interfaces · B1V2 2.4 Type Inference · B1V1 8.4 (anonymous classes vs lambdas)
On this page
1Concept / Definition
A lambda expression is an anonymous function written as a parameter list, the arrow token ->, and a body. It has no name, no declared return type and no throws clause: all of these come from the functional interface that is the lambda's target type. A lambda is only legal where such a target type exists — an assignment, method argument, return statement or cast.

2Why It Exists
Lambdas replace the most common use of anonymous inner classes — implementing a single-method interface — with a compact expression. The compiler infers everything it can, so the code shows only the parameters and the logic.
Lambdas also changed how Java APIs are designed. Libraries such as streams, CompletableFuture, Comparator.comparing and Map.computeIfAbsent take behaviour as an argument, so callers pass a few characters of logic instead of writing a class. Without a short syntax for functions, these APIs would be too verbose to use.

3Core Concepts
Parameters
Parameters appear in parentheses, separated by commas. Their types are usually inferred from the functional interface; they can also be declared explicitly, or with var (Java 11), which allows annotations such as @NonNull var o. Within one lambda, either all parameters are typed or none are, and var cannot be mixed with explicit types. A single inferred parameter may omit the parentheses (o -> …); zero parameters need empty parentheses (() -> …). Since Java 22, an unused parameter can be written as _ (unnamed variables, JEP 456).

Expression body
If the body is a single expression, its value is the lambda's result, with no return keyword and no braces: (a, b) -> a + b. For interfaces returning void, an expression body may be a statement expression such as a method call — s -> System.out.println(s) — whose value, if any, is discarded. This is why the same lambda s -> list.add(s) is compatible with both Consumer<String> and Predicate<String>.
Statement body
A body in braces is a block: it may declare local variables, contain several statements and must return a value on every path if the interface returns a value. Keep block bodies short; if a lambda needs more than a few lines, extract a named method and use a method reference, which is easier to read, test and see in stack traces.
Target typing and the functional interface
The same lambda text can have different types in different contexts. The compiler finds the target functional interface, takes its single abstract method as the signature, and checks the lambda's parameters, body and exceptions against it. Checked exceptions are allowed only if the interface method declares them — which the standard interfaces do not.

4How It Works Internally
Unlike anonymous classes, lambdas do not produce a .class file per lambda. javac moves the body into a private synthetic method (named like lambda$main$0) and emits an invokedynamic instruction. The first time it executes, the bootstrap method LambdaMetafactory.metafactory creates a hidden class implementing the functional interface and returns a factory for instances. Non-capturing lambdas return the same singleton instance on every evaluation; capturing lambdas create a new instance holding the captured values (1.3).

$ javap -p -c Demo.class
public static void main(java.lang.String[]);
Code:
// InvokeDynamic #0:compare:()Ljava/util/Comparator;
0: invokedynamic #7, 0
5: astore_1
…
private static int lambda$main$0(Order, Order); // the lambda body5APIs / Features / Syntax
Runnable r = () -> System.out.println("tick");
Predicate<String> p = s -> s.isBlank();
Comparator<Order> c = (a, b) -> a.total().compareTo(b.total());
BiFunction<Integer, Integer, Integer> add = (Integer a, Integer b) -> a + b;
Function<String, Integer> len = (@NonNull var s) -> s.length(); // Java 11
BinaryOperator<Integer> max = (a, b) -> {
if (a >= b) return a;
return b;
};
map.forEach((_, v) -> total.add(v)); // Java 22+: unnamed parameter
Object o = (Runnable) () -> {}; // cast supplies the target type6Practical Examples
Example 1 — Choosing the right body form
The same sorting and filtering logic can be written with the shortest form that stays readable. Expression bodies suit one-line logic; a block body is justified only when the logic needs a local variable or several steps.
List<Order> orders = repository.findAll();
// expression body: one comparison, no braces, no return
orders.sort((a, b) -> a.total().compareTo(b.total()));
// block body: needs a local variable and two steps
orders.removeIf(o -> {
BigDecimal limit = creditLimits.get(o.customerId());
return limit != null && o.total().compareTo(limit) > 0;
});
// better: extract the block into a named method
orders.removeIf(this::exceedsCreditLimit);
private boolean exceedsCreditLimit(Order o) {
BigDecimal limit = creditLimits.get(o.customerId());
return limit != null && o.total().compareTo(limit) > 0;
}The named method can be unit-tested on its own, appears with a readable name in stack traces, and explains the rule without a comment.
Example 2 — Overload ambiguity with ExecutorService.submit
ExecutorService has submit(Runnable) and submit(Callable<T>). The compiler picks the overload from the lambda's body, which can surprise readers.
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<?> f1 = pool.submit(() -> log.info("started")); // Runnable
Future<Integer> f2 = pool.submit(() -> 42); // Callable<Integer>
// a block that throws a checked exception only fits Callable
Future<String> f3 = pool.submit(() -> {
return Files.readString(Path.of("config.yml")); // IOException allowed
});
// make the intent explicit when it matters
Callable<String> task = () -> Files.readString(Path.of("config.yml"));
Future<String> f4 = pool.submit(task);Assigning the lambda to a typed variable first removes any doubt about which overload is chosen and documents whether a result is expected.
7Common Problems & Mistakes
var x = () -> 1;— no target type, compile error.- Ambiguous overloads:
submit(Runnable)andsubmit(Callable)with() -> work()— resolved by return value; lambdas with block bodies can surprise. Cast or extract a method if unclear. - Checked exceptions inside lambdas for standard interfaces — wrap, handle locally, or define a custom interface (2.3).
- Long block bodies — hard to debug; extract a method.
- Returning a value from a block body on some paths but not others — compile error.
8Performance & Production Considerations
The first evaluation of each lambda call site pays a one-time bootstrap cost (class spinning), noticeable only in startup-sensitive applications with thousands of lambdas; CDS archives and AOT caching (Java 24–25) reduce it. After that, non-capturing lambdas are free singletons and capturing lambdas cost a small allocation that escape analysis often removes. Stack traces show synthetic lambda$… frames; naming methods and using method references makes production traces more readable.
9Architecture & Design Considerations
Lambdas make it cheap to pass behaviour around, which favours APIs built from small functional parameters: a retry helper that takes a Supplier<T>, a repository method that takes a Predicate, or a validation pipeline built from Function steps. This keeps policies separate from mechanisms and makes them easy to replace in tests.
The same ease can hurt a large codebase. Business rules hidden in anonymous lambdas spread across services are hard to find, reuse and review. As a rule, keep lambdas for short glue logic at the call site, and give any rule the business cares about a name: a method, a class implementing the functional interface, or an enum constant holding the function. When designing your own APIs, accept the standard java.util.function interfaces where they fit, and define a named functional interface only when the name or a checked exception adds meaning.
10Interview Questions & Answers
What are the parts of a lambda expression?
A lambda has three parts: a parameter list, the arrow token ->, and a body. The parameter list can be empty, as in () -> …, a single name, as in s -> …, or several names in parentheses, with or without types. The body is either a single expression, whose value is returned automatically, or a block in braces that uses return statements when a value is needed. A lambda has no name, return type or throws clause of its own; these all come from the functional interface it is assigned to, which is called its target type.
When can you omit parentheses and types in a lambda?
Parameter types can be left out whenever the compiler can infer them from the target functional interface, which is almost always the case. If you declare types, you must declare them for every parameter, either explicitly or all with var; mixing typed and untyped parameters does not compile. Parentheses can be left out only when there is exactly one parameter and it has no declared type, as in s -> s.trim(). Zero parameters need empty parentheses, and two or more always need parentheses. Using var is useful when you want to add an annotation such as @NonNull to a parameter.
What is the difference between an expression body and a statement body?
An expression body is a single expression without braces, such as (a, b) -> a + b, and its value is returned automatically. If the target method returns void, the expression can be a method call whose result is simply ignored. A statement body is a block in braces: it can declare local variables and run several statements, and if the method returns a value, every path must end with a return statement. Use an expression body for one-line logic. When a block grows beyond a few lines, move it into a named method and use a method reference.
Why does var f = x -> x * 2; not compile?
A lambda has no type of its own. The compiler can only type it from its context, the target functional interface, such as an assignment to Function<Integer, Integer>, a method parameter or a cast. var asks the compiler to infer the variable's type from the right-hand side, but the right-hand side needs the variable's type to be typed, so there is nothing to infer from and javac reports an error. The fix is to declare the interface, as in Function<Integer, Integer> f = x -> x * 2, or to cast the lambda to the interface type.
How do lambdas differ from anonymous inner classes?
A lambda can implement only a functional interface, while an anonymous class can extend a class or implement an interface with several methods and can have its own fields and state. Inside a lambda, this refers to the enclosing instance, and the lambda does not introduce a new scope, so it cannot redeclare a local variable from the enclosing method; an anonymous class has its own this and scope. Lambdas are compiled to a private method and an invokedynamic call site instead of a separate class file, and non-capturing lambdas are reused as a single instance.
How is a lambda implemented at run time?
javac moves the lambda's body into a private synthetic method, with a name such as lambda$main$0, and replaces the lambda expression with an invokedynamic instruction. The first time that instruction runs, its bootstrap method, LambdaMetafactory.metafactory, creates a hidden class that implements the functional interface and calls the synthetic method. The call site is then linked, so later evaluations skip the bootstrap. A lambda that captures nothing returns the same instance every time; a capturing lambda creates a new small object holding the captured values. Method references use the same mechanism, without the synthetic method.
How would you keep lambda-heavy code readable and debuggable in a large codebase?
Set a few team rules and enforce them in code review and static analysis. Keep lambdas short, ideally one expression, and move longer logic into well-named private methods used through method references, so stack traces and test names describe what happens. Avoid nested lambdas and long chains that mix several concerns; split a stream pipeline into named steps when it grows. Give variables that hold functions descriptive names, such as isOverdue rather than p. Use explicit parameter types when inference hides meaning. For business rules, prefer named methods or classes so they are easy to find, reuse and test.