Why Do Local Variables Used in Lambdas Have to Be Final or Effectively Final? – 为什么Lambdas中使用的局部变量必须是最终变量或有效的最终变量?

最后修改: 2019年 4月 13日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.介绍

Java 8 gives us lambdas, and by association, the notion of effectively final variables. Ever wondered why local variables captured in lambdas have to be final or effectively final?

Java 8为我们提供了lambdas,并通过关联,提供了effectively final变量的概念。有没有想过,为什么在lambdas中捕获的局部变量必须是final或effective final的?

Well, the JLS gives us a bit of a hint when it says “The restriction to effectively final variables prohibits access to dynamically-changing local variables, whose capture would likely introduce concurrency problems.” But, what does it mean?

好吧,JLS给了我们一点提示,它说:”对有效最终变量的限制禁止访问动态变化的局部变量,捕获这些变量可能会引入并发问题。”但是,它是什么意思呢?

In the next sections, we’ll dig deeper into this restriction and see why Java introduced it. We’ll show examples to demonstrate how it affects single-threaded and concurrent applications, and we’ll also debunk a common anti-pattern for working around this restriction.

在接下来的章节中,我们将更深入地探讨这一限制,看看为什么Java会引入这一限制。我们将举例说明它是如何影响单线程和并发应用程序的,并且我们还将驳斥一个常见的反模式,以绕过这一限制。

2. Capturing Lambdas

2.捕获Lambdas

Lambda expressions can use variables defined in an outer scope. We refer to these lambdas as capturing lambdas. They can capture static variables, instance variables, and local variables, but only local variables must be final or effectively final.

Lambda表达式可以使用定义在外部作用域中的变量。我们将这些lambdas称为捕获lambdas。它们可以捕获静态变量、实例变量和局部变量,但只有局部变量必须是最终变量或有效的最终变量。

In earlier Java versions, we ran into this when an anonymous inner class captured a variable local to the method that surrounded it – we needed to add the final keyword before the local variable for the compiler to be happy.

在早期的Java版本中,当一个匿名的内层类捕获了一个包围它的方法的局部变量时,我们就会遇到这种情况–我们需要在局部变量前添加final 关键字,这样编译器才会满意。

As a bit of syntactic sugar, now the compiler can recognize situations where, while the final keyword isn’t present, the reference isn’t changing at all, meaning it’s effectively final. We could say that a variable is effectively final if the compiler wouldn’t complain were we to declare it final.

作为语法糖,现在编译器可以识别这样的情况:虽然final 关键字不存在,但引用根本没有变化,这意味着它是effectively final。我们可以说一个变量实际上是最终的,如果我们声明它是最终的,编译器就不会抱怨。

3. Local Variables in Capturing Lambdas

3.捕获Lambdas中的局部变量

Simply put, this won’t compile:

简单地说,这将不会被编译:

Supplier<Integer> incrementer(int start) {
  return () -> start++;
}

start is a local variable, and we are trying to modify it inside of a lambda expression.

start 是一个局部变量,我们试图在一个lambda表达式中修改它。

The basic reason this won’t compile is that the lambda is capturing the value of start, meaning making a copy of it. Forcing the variable to be final avoids giving the impression that incrementing start inside the lambda could actually modify the start method parameter.

这不会被编译的基本原因是,lambda正在捕获start的值,即对其进行复制。强制该变量为final,可以避免让人觉得在lambda内增加start实际上可以修改start方法参数。

But, why does it make a copy? Well, notice that we are returning the lambda from our method. Thus, the lambda won’t get run until after the start method parameter gets garbage collected. Java has to make a copy of start in order for this lambda to live outside of this method.

但是,为什么它要进行复制呢?嗯,注意到我们正在从我们的方法中返回lambda。因此,在start方法参数被垃圾回收后,这个lambda才会被运行。Java必须为start制作一个副本,以便这个lambda能够在这个方法之外生存。

3.1. Concurrency Issues

3.1.并发问题

For fun, let’s imagine for a moment that Java did allow local variables to somehow remain connected to their captured values.

为了好玩,让我们想象一下,Javadid允许局部变量以某种方式与它们的捕获值保持联系。

What should we do here:

我们应该在这里做什么。

public void localVariableMultithreading() {
    boolean run = true;
    executor.execute(() -> {
        while (run) {
            // do operation
        }
    });
    
    run = false;
}

While this looks innocent, it has the insidious problem of “visibility”. Recall that each thread gets its own stack, and so how do we ensure that our while loop sees the change to the run variable in the other stack? The answer in other contexts could be using synchronized blocks or the volatile keyword.

虽然这看起来很无辜,但它有一个阴险的问题,那就是 “可见性”。回顾一下,每个线程都有自己的堆栈,那么我们如何确保我们的while循环看到run变量在其他堆栈中的变化?在其他情况下,答案可能是使用同步块或volatile关键字。

However, because Java imposes the effectively final restriction, we don’t have to worry about complexities like this.

然而,由于Java施加了有效的最终限制,我们不必担心像这样的复杂问题。

4. Static or Instance Variables in Capturing Lambdas

4.捕获Lambdas的静态变量或实例变量

The examples before can raise some questions if we compare them with the use of static or instance variables in a lambda expression.

如果我们将前面的例子与lambda表达式中的静态变量或实例变量的使用进行比较,会引起一些问题。

We can make our first example compile just by converting our start variable into an instance variable:

我们可以通过将start变量转换为实例变量来使我们的第一个例子得到编译。

private int start = 0;

Supplier<Integer> incrementer() {
    return () -> start++;
}

But, why can we change the value of start here?

但是,为什么我们可以在这里改变start的值?

Simply put, it’s about where member variables are stored. Local variables are on the stack, but member variables are on the heap. Because we’re dealing with heap memory, the compiler can guarantee that the lambda will have access to the latest value of start.

简单地说,它是关于成员变量的存储位置。本地变量在堆栈中,但成员变量在堆中。因为我们在处理堆内存,编译器可以保证λ可以访问start.的最新值。

We can fix our second example by doing the same:

我们可以通过同样的方法解决我们的第二个例子。

private volatile boolean run = true;

public void instanceVariableMultithreading() {
    executor.execute(() -> {
        while (run) {
            // do operation
        }
    });

    run = false;
}

The run variable is now visible to the lambda even when it’s executed in another thread since we added the volatile keyword.

由于我们添加了volatile关键字,即使lambda在另一个线程中执行,run变量现在也是可见的。

Generally speaking, when capturing an instance variable, we could think of it as capturing the final variable this. Anyway, the fact that the compiler doesn’t complain doesn’t mean that we shouldn’t take precautions, especially in multithreading environments.

一般来说,当捕获一个实例变量时,我们可以认为是捕获最终变量this。无论如何,编译器不抱怨的事实并不意味着我们不应该采取预防措施,尤其是在多线程环境中。

5. Avoid Workarounds

5.避免变通

In order to get around the restriction on local variables, someone may think of using variable holders to modify the value of a local variable.

为了绕过对局部变量的限制,有人会想到使用变量持有者来修改局部变量的值。

Let’s see an example that uses an array to store a variable in a single-threaded application:

让我们看一个例子,在一个单线程的应用程序中使用数组来存储一个变量。

public int workaroundSingleThread() {
    int[] holder = new int[] { 2 };
    IntStream sums = IntStream
      .of(1, 2, 3)
      .map(val -> val + holder[0]);

    holder[0] = 0;

    return sums.sum();
}

We could think that the stream is summing 2 to each value, but it’s actually summing 0 since this is the latest value available when the lambda is executed.

我们可以认为数据流对每个值的求和是2,但它实际上是在求和0,因为这是执行λ时的最新值。

Let’s go one step further and execute the sum in another thread:

让我们再进一步,在另一个线程中执行和。

public void workaroundMultithreading() {
    int[] holder = new int[] { 2 };
    Runnable runnable = () -> System.out.println(IntStream
      .of(1, 2, 3)
      .map(val -> val + holder[0])
      .sum());

    new Thread(runnable).start();

    // simulating some processing
    try {
        Thread.sleep(new Random().nextInt(3) * 1000L);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }

    holder[0] = 0;
}

What value are we summing here? It depends on how long our simulated processing takes. If it’s short enough to let the execution of the method terminate before the other thread is executed it’ll print 6, otherwise, it’ll print 12.

我们在这里求的是什么值?这取决于我们的模拟处理需要多长时间。如果时间短到足以让方法的执行在其他线程执行之前终止,它将打印6,否则,它将打印12。

In general, these kinds of workarounds are error-prone and can produce unpredictable results, so we should always avoid them.

一般来说,这类变通方法容易出错,而且会产生不可预知的结果,所以我们应该始终避免它们。

6. Conclusion

6.结论

In this article, we’ve explained why lambda expressions can only use final or effectively final local variables. As we’ve seen, this restriction comes from the different nature of these variables and how Java stores them in memory. We’ve also shown the dangers of using a common workaround.

在这篇文章中,我们已经解释了为什么lambda表达式只能使用final或有效final局部变量。正如我们所看到的,这种限制来自于这些变量的不同性质以及Java在内存中存储它们的方式。我们还展示了使用一个常见的变通方法的危险性。

As always, the full source code for the examples is available over on GitHub.

一如既往,这些示例的完整源代码可在GitHub上获取