Behavior You Can Pass Around
A lambda expression is a compact block of code you can hand to a method, store in a variable, or return - just like you would a number or a string. Before lambdas, passing behavior meant writing a whole class (or a verbose anonymous class) just to wrap one method. A lambda boils that down to its essence: the parameters and the body.
A lambda always implements a functional interface - an interface with exactly one abstract method (you met these at the end of the interfaces page). The compiler figures out which interface you mean from the context and matches your lambda to that single method.
x -> x * 2 is the entire implementation of apply. No new, no class body, no method name - the interface supplies all of that.
The Arrow Syntax
Every lambda has the shape parameters -> body. The pieces flex depending on how much you need:
() -> 42 // no parameters
x -> x + 1 // one parameter, parentheses optional
(x, y) -> x + y // two or more parameters need parentheses
(int x, int y) -> x + y // types are optional - usually inferred
x -> { // a block body needs braces and return
int doubled = x * 2;
return doubled + 1;
}
A single expression body (x -> x + 1) implicitly returns its value - no return keyword. The moment you use braces, you are writing a normal block and must return explicitly if the method returns something. A common mistake is mixing the two: x -> { x + 1 } does not compile, because a block needs a statement (return x + 1;).
Lambdas Replace Anonymous Classes
The clearest way to see what a lambda buys you is the before/after. Sorting with a custom Comparator used to look like this:
// before - anonymous class
names.sort(new Comparator<String>() {
public int compare(String a, String b) {
return a.length() - b.length();
}
});
The same thing as a lambda is one readable line:
Comparator is a functional interface (its one abstract method is compare), so the lambda slots straight in. All the ceremony - the new, the class, the method signature - is gone, leaving only the comparison logic.
The java.util.function Toolbox
You rarely need to declare your own functional interface. The java.util.function package ships the common shapes, and almost all of Java's library APIs accept them:
Function<T, R>- takes aT, returns anR(apply)Predicate<T>- takes aT, returns aboolean(test)Supplier<T>- takes nothing, returns aT(get)Consumer<T>- takes aT, returns nothing (accept)
These are generic interfaces - Function<String, Integer> reuses the generics you saw on the previous page to stay type-safe. Pick the one whose shape matches what your code needs to receive and produce.
Method References
When a lambda does nothing but call one existing method, you can replace it with a method reference using ::. It is the same value, written more directly:
Method references come in flavors: String::toUpperCase (an instance method called on each argument), Math::max (a static method), System.out::println (a method on a specific object), and ArrayList::new (a constructor). Reach for one only when it reads clearly - if you have to think about which form applies, a plain lambda is fine.
Capturing Variables
A lambda can use local variables from the method around it, but only if they are final or effectively final - assigned once and never changed. Java captures the value at the moment the lambda is created, so a variable that could later change would be ambiguous.
If you reassign factor anywhere, the compiler rejects the lambda with "variable used in lambda expression should be final or effectively final." When you genuinely need mutable shared state, capture an object instead - a field, an array element, or an AtomicInteger - because the reference stays fixed even though its contents change. Note that lambdas, unlike anonymous classes, do not create their own scope: this inside a lambda refers to the enclosing instance, not the lambda itself.
Next: Streams
Lambdas are the building block, not the destination. Their real payoff comes with the Streams API, where you chain operations like filter, map, and reduce - each taking a lambda - to express data transformations as a readable pipeline instead of a tangle of loops. That is the next page.
Frequently Asked Questions
What is a lambda expression in Java?
A lambda is a short way to write an instance of a functional interface - an interface with a single abstract method. Instead of a whole anonymous class, you write parameters -> body. The compiler matches the lambda to the interface's one method, so Runnable r = () -> System.out.println("hi"); is a complete Runnable. Lambdas let you pass behavior around like data.
What is the difference between a lambda and a method reference in Java?
Both produce an instance of a functional interface. A lambda has explicit parameters and a body (s -> s.toUpperCase()), while a method reference is shorthand for a lambda that just calls one existing method (String::toUpperCase). Use a method reference when the lambda would do nothing but forward its arguments to a single named method - it is shorter and reads better.
Why must variables used in a Java lambda be final or effectively final?
A lambda can capture local variables from the enclosing method, but only if they never change after assignment - that is what "effectively final" means. Java captures the value, not a live reference to the variable, so allowing reassignment would be ambiguous and unsafe across threads. If you need mutable shared state, use a field or an array/AtomicInteger wrapper instead.