3.7 Lambda Expressions and Variable Scope
In the following sections, you will learn how variables work inside lambda expressions. This information is somewhat technical but essential for working with lambda expressions.
3.7.1 Scope of a Lambda Expression
The body of a lambda expression has the same scope as a nested block. The same rules for name conflicts and shadowing apply. It is illegal to declare a parameter or a local variable in the lambda that has the same name as a local variable.
int first = 0;
Comparator<String> comp = (first, second) -> first.length() - second.length();
// Error: Variable first already defined
Inside a method, you can’t have two local variables with the same name, therefore you can’t introduce such variables in a lambda expression either.
As another consequence of the “same scope” rule, the this keyword in a lambda expression denotes the this parameter of the method that creates the lambda. For example, consider
public class Application() {
public void doWork() {
Runnable runner = () -> { ...; System.out.println(this.toString()); ... };
...
}
}
The expression this.toString() calls the toString method of the Application object, not the Runnable instance. There is nothing special about the use of this in a lambda expression. The scope of the lambda expression is nested inside the doWork method, and this has the same meaning anywhere in that method.
3.7.2 Accessing Variables from the Enclosing Scope
Often, you want to access variables from an enclosing method or class in a lambda expression. Consider this example:
public static void repeatMessage(String text, int count) {
Runnable r = () -> {
for (int i = 0; i < count; i++) {
System.out.println(text);
}
};
new Thread(r).start();
}
Note that the lambda expression accesses the parameter variables defined in the enclosing scope, not in the lambda expression itself.
Consider a call
repeatMessage("Hello", 1000); // Prints Hello 1000 times in a separate thread
Now look at the variables count and text inside the lambda expression. If you think about it, something nonobvious is going on here. The code of the lambda expression may run long after the call to repeatMessage has returned and the parameter variables are gone. How do the text and count variables stay around when the lambda expression is ready to execute?
To understand what is happening, we need to refine our understanding of a lambda expression. A lambda expression has three ingredients:
- A block of code
- Parameters
- Values for the free variables—that is, the variables that are not parameters and not defined inside the code
In our example, the lambda expression has two free variables, text and count. The data structure representing the lambda expression must store the values for these variables—in our case, "Hello" and 1000. We say that these values have been captured by the lambda expression. (It's an implementation detail how that is done. For example, one can translate a lambda expression into an object with a single method, so that the values of the free variables are copied into instance variables of that object.)
As you have seen, a lambda expression can capture the value of a variable in the enclosing scope. To ensure that the captured value is well defined, there is an important restriction. In a lambda expression, you can only reference variables whose value doesn't change. This is sometimes described by saying that lambda expressions capture values, not variables. For example, the following is a compile-time error:
for (int i = 0; i < n; i++) {
new Thread(() -> System.out.println(i)).start();
// Error—cannot capture i
}
The lambda expression tries to capture i, but this is not legal because i changes. There is no single value to capture. The rule is that a lambda expression can only access local variables from an enclosing scope that are effectively final. An effectively final variable is never modified—it either is or could be declared as final.
As a consequence of the “effectively final” rule, a lambda expression cannot mutate any captured variables. For example,
public static void repeatMessage(String text, int count, int threads) {
Runnable r = () -> {
while (count > 0) {
count--; // Error: Can't mutate captured variable
System.out.println(text);
}
};
for (int i = 0; i < threads; i++) new Thread(r).start();
}
This is actually a good thing. As you will see in Chapter 10, if two threads update count at the same time, its value is undefined.