1.6 变量作用域

1.6 变量作用域

通常,我们希望能够在lamba 表达式的闭合方法或类中访问其他的变量,例如:
 

  1. public static void repeatMessage(String text, int count) {  
  2. Runnable r = () -> {  
  3. for (int i = 0; i < count; i++) {  
  4. System.out.println(text);  
  5. Thread.yield();  
  6. }  
  7. };  
  8. new Thread(r).start();  

假设有以下调用:

repeatMessage("Hello", 1000); //在另一个线程中打印Hello 一千次注意看lambda 表达式中的变量count 和text,它们并没有在lambda 表达式中被定义,而是方法repeatMessage 的参数变量。

如果你思考一下,就会发现这里有一些隐含的东西。lambda 表达式可能会在repeatMessage 返回之后才运行,此时参数变量已经消失了。如果保留text 和count 变量会怎样呢?

为了理解这一点,我们需要对lambda 表达式有更深入的理解。一个lambda 表达式包括三个部分:

1. 一段代码。

2. 参数。

3. 自由变量的值,这里的“自由”指的是那些不是参数并且没有在代码中定义的变量。

在我们的示例中,lambda 表达式有两个自由变量,text 和count。数据结构表示lambda 表达式必须存储这两个变量的值,即"Hello"和1000。我们可以说,这些值已经被lambda 表达式捕获了。(这是一个技术实现的细节。例如,你可以将一个lambda表达式转换为一个只含一个方法的对象,这样自由变量的值就会被复制到该对象的实例变量中。)

注意:含有自由变量的代码块被称之为“闭包(closure)”。如果有人得意洋洋地宣传他们的语言有闭包,你可以放心,Java 也有。在Java 中,lambda 表达式就是闭包。事实上,内部类一直都是闭包。Java 8 中为闭包赋予了更吸引人的语法。

如你所见,lambda 表达式可以捕获闭合作用域中的变量值。在Java 中,为了确保被捕获的值是被良好定义的,需要遵守一个重要的约束。在lambda 表达式中,被引用的变量的值不可以被更改。例如,下面这个表达式是不合法的:
 

  1. public static void repeatMessage(String text, int count) {  
  2. Runnable r = () -> {  
  3. while (count > 0) {  
  4. count--; // 错误,不能更改已捕获变量的值  
  5. System.out.println(text);  
  6. Thread.yield();  
  7. }  
  8. };  
  9. new Thread(r).start();  

做出这个约束是有原因的。更改lambda 表达式中的变量不是线程安全的。假设有一系列并发的任务,每个线程都会更新一个共享的计数器。
 

  1. int matches = 0;  
  2. for (Path p : files)  
  3. new Thread(() -> { if (p 中包含某些属性) matches++; }).start();  
  4. //非法更改matches 的值 

如果这段代码是合法的,那么会引起十分糟糕的结果。自增操作matches++不是原子操作,如果多个线程并发执行该自增操作,天晓得会发生什么。

注意:内部类也会捕获闭合作用域中的值。在Java 8之前,内部类只允许访问final的局部变量。为了适应lambda 表达式,这条规则现在也被放宽了。一个内部类可以访问任何有效的final 局部变量——即任何值不会发生变化的变量。

不要指望编译器会捕获所有并发访问错误。不可变的约束只作用在局部变量上。如果matches 是一个实例变量或者某个闭合类的静态变量,那么不会有任何错误被报告出来,即使结果同样未定义。

同样,改变一个共享对象也是完全合法的,即使这样并不恰当。例如,
 

  1. List<Path> matches = new ArrayList<>();  
  2. for (Path p : files)  
  3. new Thread(() -> { if (p 中包含某些属性) matches.add(p); }).start();  
  4. // 你可以改变matches 的值,但是在多线程下是不安全的 

注意matches 是“有效final”的(一个有效的final 变量被初始化后,就永远不会再被赋予一个新值的变量)。在我们的示例中,matches 总是引用同一个ArrayList对象。但是,这个对象是可变的,因此是线程不安全的。如果多个线程同时调用add 方法,结果将无法预测。

其实也有能够并发计数和收集值的线程安全的机制。在第2 章中,你将会学习如何使用stream 来收集具有某些属性的值。在其他情况下,你可能希望使用线程安全的计数器和集合。关于这一重要话题的更多信息,请参考第6 章。

注意:同内部类一样,我们也有一种巧妙的方式,能够让lambda 表达式更新一个闭合、局部作用域中的计数器。我们可以使用一个长度为1 的数组,如下所示。
 

  1. int[] counter = new int[1];  
  2. button.setOnAction(event -> counter[0]++); 

当然,像这样的代码不是线程安全的。对于一个按钮的回调方法来说,这无关紧要,但是一般来说,如果你真的想使用这种小技巧,还是应当三思而行。在第6 章你将会学习如何实现一个线程安全的共享计数器。

lambda 表达式的方法体与嵌套代码块有着相同的作用域。因此它也适用同样的命名冲突和屏蔽规则。在lambda 表达式中不允许声明一个与局部变量同名的参数或者局部变量。
 

  1. Path first = Paths.get("/usr/bin");  
  2. Comparator<String> comp =  
  3. (first, second) -> Integer.compare(first.length(), second.length());  
  4. // 错误:变量first 已经被定义了 

在一个方法中,你不能有两个同名的局部变量,因此,你也不能在lambda 表达式中引入这样的变量。

当你在lambda 表达式中使用this 关键字时,你会引用创建该lambda 表达式的方法的this 参数,以下面的代码为例:
 

  1. public class Application {  
  2. public void doWork() {  
  3. Runnable runner = () -> { ...; System.out.println(this.toString()); ... };  
  4. ...  
  5. }  

表达式this.toString()会调用Application 对象的toString 方法,而不是Runnable 实例的toString 方法。在lambda 表达式中使用this,与在其他地方使用this 没有什么不同。lambda 表达式的作用域被嵌套在doWork 方法中,并且无论this 位于方法的何处,其意义都是一样的。

posted @ 2016-12-22 14:04  叶枫声  阅读(148)  评论(0编辑  收藏  举报