Java Lambda Expressions
›Short versionOpen if you only have a minute.
- A lambda implements the single abstract method of a functional interface. It has no type until a target type supplies one.
- (a, b) -> a + b is an expression body. Braces make a block, and a value comes back only through return.
- Consumer accepts a value. Supplier provides one. Predicate tests one. Function turns a T into an R.
- A lambda may capture a local only when that local is final or effectively final: initialized, then never assigned.
- The captured local is copied. The copy would diverge if the variable changed, so the compiler rejects the assignment.
- A field is read through the enclosing instance on each call. Changing the field is visible. this inside a lambda is that enclosing instance.
A is the body of one method, written at the place you would otherwise have built a class. It can be passed around because a has exactly one abstract method for that body to fill. The subtle part is what the body closes over. A local variable is by copying it, and the copy is allowed only when the variable is . A field is read live, through the object.
The context picks the type
The arrows are not a type. means the compiler looks at the assignment, the argument, or the return type, finds a functional interface, and checks the lambda against that one method.
@FunctionalInterface
interface IntOperation {
int apply(int left, int right);
}
IntOperation add = (a, b) -> a + b;
IntOperation multiply = (a, b) -> a * b;
System.out.println(add.apply(3, 4)); // 7
System.out.println(multiply.apply(3, 4)); // 12@FunctionalInterface is optional for the lambda to work. It makes a second abstract method a compile error on the interface, so the target stays something a lambda can implement. The same idea shows up in forEach, which takes a Consumer.
names.forEach(name -> System.out.println(name));
names.forEach(System.out::println);Four interfaces in java.util.function cover most call sites. Consumer<T> takes a value and returns nothing. Supplier<T> takes nothing and returns a value. Predicate<T> takes a value and returns a boolean. Function<T, R> takes a T and returns an R. A such as System.out::println is the form you use when a method already has that shape.
What the lambda is allowed to remember
The lambda may run later, on another thread, after the method that built it has returned. A local variable lives in that method’s stack frame. The frame will be gone. So the compiler copies the local into the lambda. The copy stays faithful only when the method never assigns the variable again. That is the effectively-final rule, and it is a compile error rather than a stale read you discover in production.
import java.util.List;
String prefix = "User: ";
List<String> names = List.of("Ada", "Linus", "Grace");
names.stream()
.map(name -> prefix + name)
.forEach(System.out::println);
// prefix = "Member: "; // illegal: prefix is captured and would no longer be effectively finalprefix is initialized and then only read, so the copy inside the map lambda is the prefix the method saw. An assignment anywhere later in the method, even after the stream line, means the variable is not effectively final, and the capture is rejected. A for index is assigned on every iteration, so a lambda inside that loop cannot capture the index. Give the iteration its own local that you do not assign, and the capture is legal again.
A field is not copied as a value. The lambda captures the enclosing instance and reads the field when it runs. Change the field before the call, and the lambda sees the new value. That is the decision the language made: locals are stable snapshots, fields stay locations on the object.
import java.util.function.Predicate;
class Limit {
int max = 10;
Predicate<Integer> under() {
return n -> n < max; // reads the field through this, on each test
}
}
Limit limit = new Limit();
Predicate<Integer> under = limit.under();
limit.max = 3;
System.out.println(under.test(5)); // false: the field changedA method sets int limit = 10, returns the lambda n -> n < limit, and then has a line that assigns limit = 3. Why does that method fail to compile?
The lambda captures limit, so the local has to be effectively final. The later assignment means the variable changes, and the compiler will not copy a value that the method can still edit. Delete the assignment and the capture is legal. If limit is a field of the enclosing object, the assignment is legal and the lambda observes it, because the field is read through this on each call rather than copied at creation.