上篇文章中我们总结了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 .
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 节有稍微细致一点的说明,内容不多,我就全部搬过来做简单说明。
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
的了。
除了上面这种情况外,还定义了另外一种场景:
In addition, a local variable whose declaration lacks an initializer is effectively final if all of the following are true:
要特别注意, 这个场景只适用于局部变量
,而上面规定的那种场景适用于局部变量、方法、构造器、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 the final
modifier to its declaration will not introduce any compile-time errors. Conversely, a local variable or parameter that is declared final
in a valid program becomes effectively final if the final
modifier is removed.
这个对于我们判断变量是不是有效的final非常有用:如果是有效的final,那显式的加上 final
关键字肯定不会产生错误。相反的,对于本身已经被final修饰的变量,并且没有任何错误的话,那去掉 final
修饰符,这个变量肯定也是有效的 final
。