上篇文章中我们总结了Lambda的一些基本知识,本文总结剩余的一个知识点:Lambda表达式中变量的作用域问题。
Lambda表达式变量作用域
这里使用书上面给的一个例子,我们定义了一个静态的repeatMessage
方法,代码如下:
public static void repeatMessage(String text, int delay) {
ActionListener listener = event -> {
System.out.println(text);
Toolkit.getDefaultToolkit().beep();
};
new Timer(delay, listener).start();
}
可以看到,这个方法里面有一个Lambda表达式,且表达式里面使用了外面方法的一个参数text
。我们知道Lambda表达式经常使用的场景就是Lambda表达式所定义的逻辑在以后某个时间点可能才会执行。也就是说repeatMessage
和里面的Lambda表达式未必是一起执行的,也许Lambda执行的时候repeatMessage
已经执行过了,其参数肯定也早已经获取不到了,所以为了解决这个问题,Lambda表达式会事先将它所引用的外部的参数拷贝一份,一般称作Lambda表达式捕获(capture)了这个变量,而捕获的诸如text
这种既不是Lambda表达式参数列表里面提供的变量,也不是Lambda body里面定义的变量,称之为自由变量(free variable)。所以这样看,一个Lambda表达式除了上文中介绍三部分外,还应该加上自由变量。
有了这个概念之后,我们先给出本文要讨论的问题的结论:Lambda表达式虽然是为了提供一种函数式编程,但它并没有创建一个新的变量作用域(scope),表达式中涉及的所有变量的作用域与定义Lambda的方法相同。
说的有点绕,举个例子。比如上面的repeatMessage
方法里面有一个Lambda表达式,表达式中使用了一个参数event
,要注意这个event
变量的作用域是整个repeatMessage
方法,而不仅仅是Lambda表达式。也就是说,如果在repeatMessage
方法里面的其它地方再定义一个event
,那就会变量名冲突。这就是并没有创建一个新的变量作用域的意思。这里我们再举几个例子:
@FunctionalInterface
interface RepeatMessage {
void repeatMessage(String message, int times);
}
public class LambdaTest {
public static void main(String[] args) throws Exception {
int times, i;
RepeatMessage repeatMessage = (msg, times) -> {
for (int i = 0; i < times; i++) {
System.out.println(msg);
}
}
}
}
上面代码中有两处错误:1. Lambda表达式的参数列表中定义了一个变量times
,这个与上面的int times
变量冲突了。2. Lambda表达式里面的循环中定义了一个变量i
,这个与外面定义的i
也冲突了。所以只要记住Lambda表达式并没有创建一个新的变量作用域即可。书中的原话是:
The body of a lambda expression has the same scope as a nested block.
有效的final (effectively final)
Java对于Lambda中捕获的自由变量有一个非常重要的限定:该变量必须是effectively final的。所谓effectively final简单理解就是这个变量一旦初始化之后就不能再重新赋值了(An effectively final variable is a variable that is never assigned a new value after it has been initialized.)。这样限定的主要原因是Lambda表达式可能会并发执行,在里面修改捕获的变量的值可能会产生问题。
其实这个"effectively final"概念在Java其它地方也有用到,所以为了更准确的理解这个概念,我专门查了一下相关文档,Lambda Specification, Part B: Lambda Expressions的4.12.4 Final Variables节有稍微细致一点的说明,内容不多,我就全部搬过来做简单说明。
场景1
Certain variables that are not declared
final
may instead be considered effectively final.A local variable or a method, constructor, lambda, or exception parameter is effectively final if it is not final but it never occurs as the left hand operand of an assignment operator (15.26) or as the operand of a prefix or postfix increment or decrement operator (15.14, 15.15).
上面定义了这样一种场景:局部变量、方法的参数、构造器的参数、Lambda的参数、异常的参数等虽然没有被final
修饰,但也没有作为复制操作符的左值(简单说就是没有被赋值),并且也没有使用++
或--
操作符。在这种情况下,即使变量没有被final
修饰,我们也可以认为它是有效的final变量(effectively final)。这个比较好理解,因为排除这几种情况的话,变量是不可能有途径改变的,自然就是final
的了。
场景2
除了上面这种情况外,还定义了另外一种场景:
In addition, a local variable whose declaration lacks an initializer is effectively final if all of the following are true:
- It is not final.
- Whenever it occurs as the left-hand operand of an assignment operator, it is definitely unassigned and not definitely assigned before the assignment (that is, it is definitely unassigned and not definitely assigned after the right-hand operand of the assignment) (16).
- It never occurs as the operand of a prefix or postfix increment or decrement operator.
要特别注意,这个场景只适用于局部变量,而上面规定的那种场景适用于局部变量、方法、构造器、Lambda、异常的参数等多种情况。这个场景简单解释就是:对于那种声明时没有初始化的局部变量,如果同时满足三个条件,那它也是有效的final:1. 没有被final
修饰。2. 该变量之前一定没有被赋过值。这个条件主要是要限定对于一个之前只声明过的变量,那它后面如果只被明确的(definitely)赋一次值,也是可以的。3. 没有使用++
或—
操作符。
最后看一些例子:
Examples of effectively final:
void m1(int x) {
int y = 1;
foo(() -> x+y);
// Legal: x and y are both effectively final.
// 这个好理解,x没有被赋值过,y只被赋值过一次。
}
void m2(int x) {
int y;
y = 1;
foo(() -> x+y);
// Legal: x and y are both effectively final.
// 这个也好理解,x没有被赋值过,y第一次只声明了,后面只被赋值过一次。
}
void m3(int x) {
int y;
if (..) y = 1;
foo(() -> x+y);
// Illegal: y is effectively final, but not definitely assigned.
// 这里y不是有效的final的原因主要是不符合"明确的赋值(definitely assigned)"的限定,因为如果if不成立,y就没有被赋值;if成立了,y又被赋值了。
}
void m4(int x) {
int y;
if (..) y = 1;
else y = 2;
foo(() -> x+y);
// Legal: x and y are both effectively final.
// 这里y声明之后,肯定会被赋值,且只会被赋一次值(不是在if中,就是在else中)
}
void m5(int x) {
int y;
if (..) y = 1;
y = 2;
foo(() -> x+y);
// Illegal: y is not effectively final.
// 这里y可能会被赋两次值
}
void m6(int x) {
foo(() -> x+1);
x++;
// Illegal: x is not effectively final.
// 这里x使用了自加操作符
}
void m7(int x) {
foo(() -> x=1);
// Illegal: x is not effectively final.
// 这里x是方法的参数,不是局部变量,所以只能用场景1来评判。而x用作了赋值操作符的左值,所以不是有效的final变量。
}
void m8() {
int y;
foo(() -> y=1);
// Illegal: y is not definitely assigned before the lambda (see 15.27.2)
// 这个稍微难理解一点,之前有说过,Lambda捕获的自由变量一定要是有效的final,而y之前只声明了,并没有只确切的初始化过一次,所以肯定不是有效的final。
}
void m9(String[] arr) {
for (String s : arr) {
foo(() -> s);
// Legal: s is effectively final (it is a new variable on each iteration)
}
}
void m10(String[] arr) {
for (int i = 0; i < arr.length; i++) {
foo(() -> arr[i]);
// Illegal: i is not effectively final (it is incremented)
// 这里i使用了自加操作符
}
}
最后,还有一段非常重要的话:
If a variable is effectively final, adding thefinal
modifier to its declaration will not introduce any compile-time errors. Conversely, a local variable or parameter that is declaredfinal
in a valid program becomes effectively final if thefinal
modifier is removed.
这个对于我们判断变量是不是有效的final非常有用:如果是有效的final,那显式的加上final
关键字肯定不会产生错误。相反的,对于本身已经被final修饰的变量,并且没有任何错误的话,那去掉final
修饰符,这个变量肯定也是有效的final
。
Reference
- Core Java Volume I
评论已关闭