局部变量 final Java_为什么在Lambdas中使用的局部变量必须是Final或有效Final

为什么在Lambdas中使用的局部变量必须是Final或有效Final

1. 介绍

Java 8提供了lambda表达式,并通过关联给出了有效final变量的概念。是否想知道为什么在lambdas中捕获的局部变量必须是final或有效的final?

JLS给了我们一些提示,它说对有效final变量的限制禁止对动态更改的本地变量的访问,捕获本地变量可能会引入并发问题。但是,这是什么意思呢?

在下一节中,我们将深入研究这一限制,并了解Java引入这一限制的原因。我们将通过一些示例来演示它如何影响单线程和并发应用程序,并且我们还将揭穿一个常见的反模式,以绕过这个限制。

2. lambda捕获

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

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

作为语法糖,现在编译器可以识别这样的情况:虽然final关键字不存在,但引用根本没有改变,这意味着它实际上是final。如果编译器不报错的话,我们可以说一个变量实际上是final。

3. lambda捕获局部变量

简单地说,这个编译不通过:

Supplier incrementer(int start) {

return () -> start++;

}

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

这编译不通过的基本原因是lambda捕获了start的值,这意味着创建了它的一个副本。强制变量为final避免给人留下这样的印象:在lambda内部递增start实际上可以修改start方法参数。

但是,为什么要复制呢?注意,我们从我们的方法返回lambda。因此,直到start方法参数被垃圾收集之后,lambda才会运行。Java必须创建start的一个副本,以使这个lambda位于这个方法之外。

3.1 并发问题

为了好玩,让我们假设Java允许局部变量以某种方式保持与它们捕获的值的连接。

这里我们应该怎么做:

public void localVariableMultithreading() {

boolean run = true;

executor.execute(() -> {

while (run) {

// do operation

}

});

run = false;

}

虽然这看起来是无害的,但它也有潜在的可见性问题。回想一下,每个线程都有自己的堆栈,那么如何确保while循环看到对另一个堆栈中的run变量的更改呢?在其他上下文中,答案可能是使用synchronized块或volatile关键字。

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

4. lambda捕获静态或实例变量

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

我们可以通过将start变量转换为实例变量来实现第一个示例的编译:

private int start = 0;

Supplier incrementer() {

return () -> start++;

}

但是,为什么我们要改变start的值呢?

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

我们可以通过同样的方法来修正第二个例子:

private volatile boolean run = true;

public void instanceVariableMultithreading() {

executor.execute(() -> {

while (run) {

// do operation

}

});

run = false;

}

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

一般来说,当捕获一个实例变量时,我们可以把它看作是捕获最后一个变量。不管怎样,编译器不会报错并不意味着我们不应该采取预防措施,尤其是在多线程环境中。

5. 变通方案

为了绕过对局部变量的限制,有人可能会考虑使用变量占位符来修改局部变量的值。

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

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();

}

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

让我们更进一步,在另一个线程中执行sum:

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;

}

这里的和是多少?这取决于我们的模拟处理需要多长时间。如果它足够短,让方法的执行在另一个线程执行之前终止,它将输出6,否则,它将输出12。

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

6. 结论

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

与往常一样,示例的完整源代码可以在GitHub上找到。over on GitHub.

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值