Java Lambda表达式(下)

上篇文章中我们总结了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 Expressions4.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 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

Reference

  • Core Java Volume I
本文链接:http://niyanchun.com/java-lambda-part2.html,转载请注明出处。
扫码关注我的微信公众号:
赞赏


微信赞赏

支付宝赞赏

添加新评论

友情提醒:填写邮箱是当别人回复你的评论时,会给邮箱发邮件提醒。