Lambdas:局部变量需要final,实例变量不需要

时间:2014-07-31 09:29:00

标签: java lambda java-8 final

在lambda中,局部变量需要是final,但实例变量不需要。为什么这样?

10 个答案:

答案 0 :(得分:55)

字段和局部变量之间的根本区别在于,当JVM创建lambda实例时,局部变量是 复制 。另一方面,字段可以自由更改,因为它们的更改也会传播到外部类实例(它们的范围是整个外部类,正如Boris在下面指出的那样)。

最简单的思考匿名类,闭包和labmdas的方法是从变量范围的角度来看;想象一下为传递给闭包的所有局部变量添加的复制构造函数。

答案 1 :(得分:21)

在项目lambda的文档中:State of the Lambda v4

7部分下。变量捕获,提到....

  

我们的意图是禁止捕获可变的局部变量。该   原因是这样的成语:

int sum = 0;
list.forEach(e -> { sum += e.size(); });
     

基本上是连续的;编写lambda体很困难   像这样没有竞争条件。除非我们愿意   强制 - 最好在编译时 - 这样的功能无法逃脱   它的捕获线程,这个功能可能会比它更麻烦   解决的问题。

修改:

这里需要注意的另一点是,当你在内部类中访问局部变量时,它们会在内部类的构造函数中传递,并且这不能使用非最终变量,因为非最终变量的值可以更改施工结束后。

在实例变量的情况下,编译器传递类和类的引用'引用将用于访问实例变量。因此,在实例变量的情况下不需要它。

PS:值得一提的是,匿名类只能访问最终的局部变量(在JAVA SE 7中),而在Java SE 8中,您可以在lambda内部以及内部类中有效地访问最终变量。

答案 2 :(得分:14)

因为实例变量总是通过对某个对象的引用的字段访问操作来访问,即some_expression.instance_variable。即使您没有通过点表示法(如instance_variable)显式访问它,也会将其隐式视为this.instance_variable(或者如果您在内部类中访问外部类的实例变量,{{1 }},这是在引擎盖OuterClass.this.instance_variable)。

因此,永远不会直接访问实例变量,而您直接访问的实际“变量”是this.<hidden reference to outer this>.instance_variable(由于它不可分配,因此是“有效的最终”),或者是开头的变量其他一些表达。

答案 3 :(得分:12)

Java 8 in Action一书中,这种情况解释为:

  

您可能会问自己为什么局部变量有这些限制。   首先,有一把钥匙   在幕后实现实例和局部变量的方式不同。例   变量存储在堆上,而局部变量存在于堆栈中。如果一个lambda可以   直接访问局部变量并在一个线程中使用lambda,然后使用该线程   lambda可以尝试在分配变量的线程之后访问变量   取消分配它。因此,Java实现对自由局部变量的访问,以访问其副本   而不是访问原始变量。如果局部变量是,则没有区别   只分配一次 - 因此限制。   其次,这种限制也阻碍了典型的命令式编程模式(正如我们所做的那样)   在后面的章节中解释,防止容易并行化)改变外部变量。

答案 4 :(得分:10)

为将来的访问者提出一些概念:

基本上,一切都归结为编译器应该能够确定性地确定lambda表达式主体不在变量的陈旧副本上工作

在使用局部变量的情况下,编译器无法确保lambda表达式主体不在该变量的陈旧副本上工作,除非该变量是final或有效的final,因此局部变量应该是final或有效的final。

现在,在使用实例字段的情况下,当您访问lambda表达式内的实例字段时,编译器会将this附加到该变量访问中(如果您未显式执行),并且自{{1} }实际上是最终的,因此编译器可以确保lambda表达式主体始终具有该变量的最新副本(请注意,在此讨论中,多线程已超出范围)。因此,在实例实例字段的情况下,编译器可以告诉lambda主体具有实例变量的最新副本,因此实例变量不必是final或有效的final。请参考以下Oracle幻灯片的屏幕截图:

enter image description here

另外,请注意,如果您要访问lambda表达式中的实例字段,并且该实例字段正在多线程环境中执行,则可能会出现问题。

答案 5 :(得分:9)

好像你在询问可以从lambda体中引用的变量。

来自JLS §15.27.2

  

使用但未在lambda表达式中声明的任何局部变量,形式参数或异常参数必须声明为final或者是有效的final(§4.12.4),否则在尝试使用时会发生编译时错误。

所以你不需要将变量声明为final,你只需要确保它们是“有效的最终”。这与适用于匿名类的规则相同。

答案 6 :(得分:6)

在Lambda表达式中,您可以有效地使用周围范围内的最终变量。 有效地意味着声明变量final并不是强制性的,但要确保不要在lambda表达式中更改其状态。

你也可以在闭包中使用它,使用“this”表示封闭对象但不是lambda本身,因为闭包是匿名函数,它们没有与之关联的类。

所以当你使用任何字段(比如说私有的Integer i;)来自封闭的类时,这个字段没有被声明为final而不是有效的最终它仍然可以工作,因为编译器会代表你并插入“this”(这个.I)。

private Integer i = 0;
public  void process(){
    Consumer<Integer> c = (i)-> System.out.println(++this.i);
    c.accept(i);
}

答案 7 :(得分:5)

这是一个代码示例,正如我没想到的那样,我预计无法修改lambda之外的任何内容

 public class LambdaNonFinalExample {
    static boolean odd = false;

    public static void main(String[] args) throws Exception {
       //boolean odd = false; - If declared inside the method then I get the expected "Effectively Final" compile error
       runLambda(() -> odd = true);
       System.out.println("Odd=" + odd);
    }

    public static void runLambda(Callable c) throws Exception {
       c.call();
    }

 }

输出:      奇数=真

答案 8 :(得分:2)

是的,您可以更改实例的成员变量,但不能更改实例本身就像处理变量时一样。

如上所述:

    class Car {
        public String name;
    }

    public void testLocal() {
        int theLocal = 6;
        Car bmw = new Car();
        bmw.name = "BMW";
        Stream.iterate(0, i -> i + 2).limit(2)
        .forEach(i -> {
//            bmw = new Car(); // LINE - 1;
            bmw.name = "BMW NEW"; // LINE - 2;
            System.out.println("Testing local variables: " + (theLocal + i));

        });
        // have to comment this to ensure it's `effectively final`;
//        theLocal = 2; 
    }

限制局部变量的基本原则是data and computation validity

  

如果由第二个线程评估的lambda被赋予了改变局部变量的能力。即使能够从不同的线程读取可变局部变量的值,也会引入同步的必要性或使用 volatile 以避免读取陈旧数据。

但我们知道principal purpose of the lambdas

  

在其中不同的原因中,Java平台最紧迫的原因是它们可以更轻松地通过多线程分发集合处理。

与本地变量完全不同,本地实例可以进行变异,因为它全局共享。我们可以通过heap and stack difference

更好地理解这一点
  

每当创建一个对象时,它总是存储在堆空间中,堆栈内存包含对它的引用。堆栈内存仅包含本地原始变量和堆空间中对象的引用变量。

总而言之,我认为有两点非常重要:

  1. 很难让实例 有效地完成,这可能会造成很多无谓的负担(想象一下深层嵌套的类)

  2. 实例本身已经全局共享,并且lambda也可以在线程之间共享,因此它们可以正常协同工作,因为我们知道我们正在处理突变并希望传递此突变;

  3. 此处的平衡点很明确:如果您知道自己在做什么,就可以轻松,但如果没有,那么默认限制将有助于避免阴险的错误。

    P.S。如果实例突变中需要同步,您可以直接使用stream reduction methods,或者如果实例突变中存在依赖性问题,则在thenApply thenCompose或类似方法时,仍然可以使用mappingusermod

答案 9 :(得分:0)

首先,在后台如何实现局部变量和实例变量有一个关键差异。实例变量存储在堆中,而局部变量存储在堆中。 如果lambda可以直接访问局部变量并且该lambda在线程中使用,则使用lambda的线程可以在分配该变量的线程将其释放后尝试访问该变量。

简而言之:为了确保另一个线程不会覆盖原始值,最好提供对copy变量的访问,而不是原始变量。