【问题标题】:Why restrict local variables to be effectively final in a Java lambda expression? [duplicate]为什么在 Java lambda 表达式中将局部变量限制为有效的最终变量? [复制]
【发布时间】:2016-04-12 01:57:02
【问题描述】:

代码:

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 表达式中将变量限制为有效的 final 呢?

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

您可以阅读我的post,了解如何在 lambda 中找到破解计数器的好方法(当然,这不是有效的 final)。

【问题讨论】:

  • 这看起来很实用,但它违背了函数式编程的要点:避免状态更改。此外:如果您需要(线程安全)计数器,请使用计数器,而不是 int。计数器可以是final。

标签: java java-8


【解决方案1】:

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

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

捕获 lambda 的工作方式类似。

所以关键是当你捕获变量时,你实际上并没有像使用引用语义那样访问外部范围,你只是拥有值的副本。

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

【讨论】:

  • This is the sort of thing Java is designed away from. 那么在您看来,这是一个好的设计吗?我认为它只是增加了一个无用的限制并导致了很多奇怪的黑客攻击......
  • “我认为它只是增加了一个无用的限制” 好的。那么你将如何解决悬空引用问题,你有一个 lambda 可以访问一个不再存在的局部变量?作为参考,这可能会导致 C++ 中的未定义行为。
  • @Radiodef 在 C# 中,每个局部变量捕获都会创建一个匿名局部类的实例,并且该变量是它的字段。变量的每次访问,无论是在方法中还是通过生成的 lambda 实例,都如同您捕获了一个实例字段一样完成。 Java 根本不想要这个。
  • @Pillar 是的,虽然我是在口头上问这个问题,因为我认为 OP 不理解他们的要求。他们说,当它实际上需要添加复杂的功能时,他们认为这是一个“无用的限制”。
  • 实际上实例变量更类似于数组成员而不是局部变量——它实际上是最终的“this”,但你可以修改它引用的内容。
【解决方案2】:

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

与 lambda 表达式或内部类共享可变局部变量的唯一方法是将其转换为字段或数组元素,换句话说,转换为共享堆变量。这将打破局部变量的基本原则,顾名思义,local,换句话说,unshared,这会对线程安全产生重大影响。

如果你对 Java 编程语言做出这样的改变,你几乎可以扔掉所有关于 Java 线程安全或并发编程的书。您突然不能依赖未共享的局部变量,因此本质上是线程安全的,您必须搜索整个方法的代码以找出是否有代码共享该变量。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-02-23
    • 1970-01-01
    • 2011-04-24
    • 1970-01-01
    相关资源
    最近更新 更多