为什么要限制局部变量在Java lambda表达式中有效最终?

时间:2016-04-12 01:57:02

标签: java java-8

代码:

int counter = 0;
int[] counterInArray = {1};
IntStream.range(1, 100)//couldn't compile since counter is not effectively final
        .forEach(x -> System.out.println(x + ": " + counter++));
IntStream.range(1, 100)//Works well
        .forEach(x -> System.out.println(x + ": " + counterInArray[0]++));
IntStream.range(1, 100).parallel()//parallel breaks the counter, so we should be careful
        .forEach(x -> System.out.println(x + ": " + counterInArray[0]++));

如你所见,我们可以做一个简单的hack(把它放到数组中)来使变量有效地最终并在单线程情况下运行良好。

那么为什么要限制变量在Java lambda表达式中有效最终?

当我们想要使用一些不是最终有效的变量时,我们必须破解它,但它适用于单线程流。

你可以阅读我的post,看看在lambda中找到一个很好的方法来破解一个计数器(当然这不是最终有效)。

2 个答案:

答案 0 :(得分:2)

捕获的变量通过复制它们的值来工作。例如,假设有一些像这样的匿名内部类:

String message = "hello world!";
new Thread(new Runnable() {
    public void run() {
        System.our.println(message);
    }
}).start();

这将编译为实际类似于以下内容的代码:

class AnonymousRunnable0 implements Runnable {
    final String messageCopy;
    AnonymousRunnable0(String message) {
        this.messageCopy = message;
    }
    public void run() {
        System.out.println(this.messageCopy);
    }
}

String message = "hello world!";
new Thread(new AnonymousRunnable0(message)).start();

以类似的方式捕捉lambdas。

所以关键是当你捕获变量时,实际上你没有访问外部作用域,就像使用引用语义一样,你只需要复制值。

此外,如果您要求是可能的,那么传递对局部变量的引用只会产生非常古怪的代码。如果捕获局部变量的lambda放在某个地方List并且该方法返回怎么办?现在您已经引用了一个不再存在的变量。这就是Java的设计之处。

答案 1 :(得分:1)

正如经常所说,从外部上下文引用局部变量当前通过复制变量的内容来工作,因此确保变量永远不会改变是一种将技术解决方案强加的行为与代码语义对齐的方法。

使用lambda表达式或内部类共享可变局部变量的唯一方法是将其转换为字段或数组元素,换句话说,将其转换为共享堆变量。这将破坏局部变量的基本原则,正如其名称所暗示的那样, local ,换句话说 unshared ,这对线程安全产生了严重影响。

如果对Java编程语言进行了这样的更改,那么几乎每本关于线程安全或Java中的并发编程的书都会被丢弃。你突然不能依赖于非共享的局部变量,因此本质上是线程安全的,你必须搜索整个方法的代码,以找出是否有代码共享该变量。

这将是一个巨大的牺牲,只是为了使沮丧的编程技术更方便。如果找不到比通过周围作用域的共享变量更好的方法来实现所需的功能,那么您可以执行已经在问题中显示的内容,使用对象或数组来显式使用堆变量。这与实现必须做的事情没有什么不同,如果它允许共享的可变局部变量,但要求使其显式不会牺牲局部变量的可靠局部性质。