局部变量 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
    评论
这个报错可能是因为您在使用 `KernelPCA` 类时,调用了 `lambdas_` 属性,但是该属性在 `KernelPCA` 类并不存在。`lambdas_` 属性是在另外一个类 `PCA` 定义的。 `KernelPCA` 类是用于实现核主成分分析(KPCA)的类,它和传统的主成分分析(PCA)有些不同,因此属性也不完全相同。如果您想要获取 KPCA 的特征值,可以使用 `eigenvalues_` 属性来获取。下面是一个示例代码: ```python from sklearn.decomposition import KernelPCA from sklearn.datasets import make_circles import matplotlib.pyplot as plt X, y = make_circles(n_samples=1000, noise=0.1, random_state=123, factor=0.5) kpca = KernelPCA(n_components=2, kernel='rbf', gamma=10) X_kpca = kpca.fit_transform(X) print(kpca.eigenvalues_) plt.scatter(X_kpca[:, 0], X_kpca[:, 1], c=y) plt.show() ``` 在上面的示例代码,我们使用 `make_circles` 生成了一个圆形数据集,然后使用 KPCA 将数据降到了二维,并可视化了降维后的结果。同时,我们使用 `kpca.eigenvalues_` 属性获取了 KPCA 的特征值,并打印了出来。 如果您想要使用传统的 PCA,也可以使用 `PCA` 类来实现。下面是一个使用 `PCA` 类的示例代码: ```python from sklearn.decomposition import PCA from sklearn.datasets import make_circles import matplotlib.pyplot as plt X, y = make_circles(n_samples=1000, noise=0.1, random_state=123, factor=0.5) pca = PCA(n_components=2) X_pca = pca.fit_transform(X) print(pca.explained_variance_ratio_) plt.scatter(X_pca[:, 0], X_pca[:, 1], c=y) plt.show() ``` 在上面的示例代码,我们同样使用了 `make_circles` 生成了一个圆形数据集,然后使用传统的 PCA 将数据降到了二维,并可视化了降维后的结果。同时,我们使用 `pca.explained_variance_ratio_` 属性获取了每个主成分的方差贡献率,并打印了出来。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值