1. Introduction to closures
Closure — a function (or a function object) that not only uses its own parameters, but also “remembers” variables from the surrounding context where it was created. Simply put, if a lambda expression or an anonymous class inside a method uses variables from that method, it becomes a closure.
A simple example
public class ClosureDemo {
public static void main(String[] args) {
String greeting = "Hello, ";
Runnable sayHello = () -> System.out.println(greeting + "world!");
sayHello.run(); // Will print: Hello, world!
}
}
Here the lambda “captured” the greeting variable from the outer method and uses it inside itself. That’s a closure!
2. Effectively final variables: what they are and why?
In Java, lambda expressions (and anonymous classes) can use only those variables from an outer method that are declared final or are not changed after initialization. Such variables are called effectively final.
Why?
This restriction exists because a lambda expression can be invoked after the method in which it was created has finished. If the variable could change, there would be confusion: which version of the variable should be used? To avoid surprises, Java requires the variable to be immutable (or at least to appear immutable).
Example: correct usage
public static void main(String[] args) {
int number = 42; // number — effectively final
Runnable r = () -> System.out.println(number);
r.run(); // 42
}
Example: attempting to change a variable after use
public static void main(String[] args) {
int number = 42;
Runnable r = () -> System.out.println(number);
number++; // ERROR: variable number must be final or effectively final
r.run();
}
The compiler will report an error: Variable used in lambda expression should be final or effectively final.
Effectively final means... a variable that is assigned exactly once and not changed afterwards. You don’t have to write final — the compiler will infer it.
3. How do lambda expressions capture variables?
When you write a lambda that uses an outer variable, Java “packages” that variable together with the lambda. Even if the method where the lambda was created has already finished, the variable doesn’t disappear — it lives inside the closure.
Illustration: a lambda “remembers” a variable
public static Runnable createGreeter(String name) {
// name is a method parameter; it will be captured by the lambda
return () -> System.out.println("Hello, " + name + "!");
}
public static void main(String[] args) {
Runnable greeter = createGreeter("Vasya");
greeter.run(); // Hello, Vasya!
}
Here, the variable name no longer exists on the stack of the main method, but greeter still “remembers” its value.
How is it implemented under the hood?
The Java compiler creates a special helper object (also called a “capture/display class”) that stores all captured variables. The lambda expression becomes an object that has a reference to this “container” with variables.
4. Closure example: returning a function that uses a variable
Let’s write a function that returns a lambda using a variable from its context:
import java.util.function.IntSupplier;
public class ClosureFactory {
public static IntSupplier makeAdder(int x) {
// x is captured by the lambda
return () -> x + 10;
}
public static void main(String[] args) {
IntSupplier adder = makeAdder(5);
System.out.println(adder.getAsInt()); // 15
}
}
Here, the variable x has already “left” the method’s stack, but the lambda can still use it.
5. Why can’t captured variables be modified?
public static void main(String[] args) {
int base = 100;
Runnable printer = () -> System.out.println(base);
base = 200; // ERROR!
printer.run();
}
The compiler won’t allow it. If we could change base, it would be unclear which version of the variable to use in the lambda: the old one or the new one? Therefore, Java forbids modifying local variables captured by a lambda.
What can be used in a lambda?
- Local variables that do not change after initialization (effectively final).
- Class fields (both static and instance) — they can be changed, but that’s a different mechanism (accessing object state), not capturing a local variable.
6. Comparison with anonymous classes
Before lambda expressions appeared in Java, you could implement closures using anonymous classes:
public static void main(String[] args) {
String word = "Java";
Runnable r = new Runnable() {
public void run() {
System.out.println(word);
}
};
r.run(); // Java
}
The same rules apply: the word variable must be final or effectively final.
Difference: the scope of this
- In an anonymous class, this refers to the instance of the anonymous class.
- In a lambda expression, this refers to the enclosing object (for example, the current instance of the class).
7. Closures and class fields
If a lambda uses a class field, this is not “capturing a local variable” in the strict sense — the field is always accessible and can be changed.
public class Counter {
private int count = 0;
public Runnable makeCounter() {
return () -> {
count++;
System.out.println("Counter: " + count);
};
}
public static void main(String[] args) {
Counter c = new Counter();
Runnable r = c.makeCounter();
r.run(); // Counter: 1
r.run(); // Counter: 2
}
}
8. Common mistakes and nuances of closures in Java
Mistake #1: attempting to modify a variable captured by a lambda. The most common mistake is trying to change a local variable after it has been used in a lambda. The compiler will report: Variable used in lambda expression should be final or effectively final.
Mistake #2: expecting the variable to be “frozen”. In Java, a captured variable is not a copy of the value but a reference to the original when it’s a class field. If the class field changes, the lambda will see the new value. But local variables used in a lambda must be effectively final.
Mistake #3: expecting a lambda to create a new scope for this. In a lambda, this is the enclosing object (the containing class). In an anonymous class, this is the anonymous class itself.
Mistake #4: using mutable objects. If you capture a reference to a mutable object (for example, a list), you can change its contents inside the lambda even if the variable itself is effectively final:
public static void main(String[] args) {
java.util.List<String> list = new java.util.ArrayList<>();
Runnable r = () -> list.add("Hello");
r.run();
System.out.println(list); // [Hello]
}
Here the list variable doesn’t change (we don’t do list = ...), but the object it references does.
GO TO FULL VERSION