Java Lambda Expressions
Concise inline implementations of functional interfaces in Java with lambda expressions: (params) -> body.
Java Lambda Expressions
A lambda expression is the concise syntax Java 8 added for "an instance of an interface that has exactly one abstract method." Before it, you wrote that as an anonymous class. After it, you write it as a parameter list, an arrow, and a body:
Runnable r = () -> System.out.println("hi");
Comparator<String> byLen = (a, b) -> a.length() - b.length();
Function<String, Integer> length = s -> s.length();There's no new kind of value here — r, byLen, and length are still object references, and at runtime each holds an instance of a class that implements the interface on the left. What's new is that the code that says "make me one" is short enough to fit at the call site, which is what unlocks every other functional idiom in the part: filter predicates, comparator builders, event handlers, stream pipelines.
The syntax forms
A lambda has three pieces: parameter list, arrow ->, and body. Each piece has shorthand:
// Zero parameters: empty parens are required
Runnable r = () -> System.out.println("tick");
// One parameter: parens optional (idiomatic to omit them)
Function<String, Integer> len = s -> s.length();
Function<String, Integer> len2 = (s) -> s.length(); // same thing
// Two or more: parens required
Comparator<String> cmp = (a, b) -> a.length() - b.length();
// Explicit types: rare but legal
BinaryOperator<Integer> add = (Integer a, Integer b) -> a + b;
// Expression body: the value of the expression is the return value
Predicate<Integer> positive = n -> n > 0;
// Block body: explicit `return` required if the interface method returns a value
Function<Integer, String> describe = n -> {
if (n == 0) return "zero";
if (n < 0) return "negative";
return "positive";
};Three rules tie those together:
- Param types are usually inferred from the target type (the interface declared at the call site). Write them only when the compiler can't pick one or when they aid readability.
- Expression body returns its value implicitly. No
return, no semicolon. The expression is the result. - Block body needs
returnwhen the interface's method has a return type. Forgetting it is a compile error, not a silentnull.
Target typing — where lambdas can appear
A lambda has no intrinsic type. The compiler determines its type from the target — the context where it's used:
Runnable r1 = () -> doWork(); // target: Runnable
Callable<Integer> c1 = () -> 42; // target: Callable<Integer>
Supplier<Integer> s1 = () -> 42; // target: Supplier<Integer>() -> 42 is the same source in all three cases, but it compiles to three different interface instances. That's why a lambda can't be assigned to Object directly — Object o = () -> 42; is ambiguous and the compiler refuses. Cast to disambiguate: Object o = (Supplier<Integer>) () -> 42;.
Targets you'll see most often:
- A method parameter typed as a functional interface:
list.removeIf(s -> s.isEmpty()). - A field or local variable of a functional-interface type:
Predicate<String> empty = String::isEmpty;. - A return type:
public Supplier<Date> now() { return Date::new; }.
If there is no target, there can be no lambda. var f = s -> s.length(); does not compile — var can't infer a target type.
Variable capture: "effectively final"
A lambda can read local variables from the enclosing method, but only if those variables are effectively final — never reassigned after their initial value:
int multiplier = 3;
IntFunction<Integer> scale = n -> n * multiplier; // OK — `multiplier` never reassigned
multiplier = 4; // <-- this line would make the lambda not compileThe rule is the same one anonymous inner classes have always had, and the reason is the same: a lambda may outlive the method it was defined in (you might store it in a field, or pass it to another thread), and Java doesn't have closures that capture the variable — it captures the value at the moment of construction. Allowing reassignment would create a confusing illusion.
Fields are a different story. A lambda can read and mutate instance and static fields freely:
class Counter {
private int n = 0;
Runnable inc = () -> n++; // legal — `n` is a field, not a local
}This is a frequent source of bugs in stream code — a lambda that mutates a shared field looks innocent but races with itself when the stream goes parallel. Pure lambdas are safer.
this, return, and break inside a lambda
A lambda is not a new scope for this. Inside a lambda, this refers to the enclosing instance — the same as the surrounding code:
class Greeter {
String prefix = "Hello, ";
Function<String, String> greet = name -> this.prefix + name; // `this` is the Greeter
}This is one of the biggest practical differences from anonymous classes, where this referred to the anonymous instance itself.
return inside a lambda returns from the lambda, not from the enclosing method. break and continue don't work in a lambda — they belong to the loop they target, and the lambda body isn't part of the surrounding loop.
Lambda vs anonymous class — when each fits
For functional interfaces, lambdas are almost always shorter and clearer. They generate slightly different bytecode (invokedynamic) and don't create a new class file per use site, so they're typically lighter at runtime too.
Use an anonymous class when:
- The interface has more than one abstract method (it isn't functional).
- You need a method-local field (
int seen = 0;accessible across calls). - You need
thisto refer to the instance you're creating, not the enclosing instance. - You need to override a default method to specialise its behaviour.
In every other case the lambda wins.
A worked example: capture, target typing, the four call sites
The program below demonstrates the four most common places a lambda appears — collection forEach, removeIf, sort, and stream filter — alongside the capture rules and target typing.
What to take from the run:
() -> \"hi\"worked as bothCallable<String>andSupplier<String>— same source, different target types, different interface instances. That's why a lambda has no type until the context provides one.times = n -> n * factorcapturedfactorby value. The compiler accepted it becausefactorwas never reassigned. Uncommentingfactor = 11would turnfactorinto a non-effectively-final variable and break the lambda's compilation.forEach,removeIf, andsortall take a different functional interface (Consumer,Predicate,Comparator), and the lambda's shape — number of parameters, presence of a return — matched each interface's single abstract method. The compiler does the matching by target typing.- The block-body
describelambda needed explicitreturnstatements because its target (Function<Integer, String>) has a non-voidreturn type. The expression-body lambdas above it returned their expression implicitly.
What's next
You know the syntax and the capture rules. The next question is: what interface, exactly, is a lambda compiling to? Java Functional Interfaces introduces the single-abstract-method (SAM) rule, the @FunctionalInterface annotation, and how to write your own functional interface for cases the standard library doesn't cover.
Practice
Which of these lambda expressions will the Java compiler reject?