Lambda:局部变量需要最终变量,实例变量不需要


75

在lambda中,局部变量需要是最终变量,而实例变量则不需要。为什么这样?



众所周知,至少在最新版本的编译器中,java 1.8局部变量仅需有效地为final,因此它们本身无需声明为final,但不能进行修改。
Valentin Ruano

在阅读完所有答案后,我仍然认为这只是由编译器强制执行的规则,旨在最大程度地减少程序员错误-也就是说,没有技术原因无法捕获可变的局部变量,或者为何可以捕获捕获的局部变量。就此而言,不要被突变。这一点得到以下事实的支持:可以使用对象包装程序轻松地规避此规则(因此,对象引用实际上是最终的,而不是对象本身)。另一种方法是创建一个数组,即Integer[] count = {new Integer(5)}。另请参阅stackoverflow.com/a/50457016/7154924
flow2k

@ McDowell,lambda不仅是匿名类的语法糖,而且是一个完全不同的构造。
和平者

Answers:


61

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

考虑到匿名类,闭包和labmdas的最简单方法是从可变范围的角度;想象一个为传递给闭包的所有局部变量添加的副本构造函数。


是的,匿名类构造函数不需要复制实例变量,因为它可以引用它。很好的解释!
杰拉德2014年

25

在lambda项目的文档中:Lambda v4的状态

在第7节“变量捕获”下,提到了...。

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

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

基本上是连续的;这样写没有竞争条件的lambda体非常困难。除非我们愿意(最好是在编译时)强制执行这样一个函数无法逃脱其捕获线程的功能,否则此功能可能会带来更多无法解决的麻烦。

编辑:

这里要注意的另一件事是,在内部类中访问局部变量时,它们会在内部类的构造函数中传递,这不适用于非最终变量,因为非最终变量的值可以在构造后更改。

在实例变量的情况下,编译器会传递类的引用,而类的引用将用于访问实例变量。因此,在实例变量的情况下不是必需的。

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


17

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

您可能会问自己,为什么局部变量具有这些限制。首先,幕后实现实例和局部变量的方式存在关键差异。实例变量存储在堆中,而局部变量存在于堆栈中。如果lambda可以直接访问局部变量并且该lambda在线程中使用,则使用lambda的线程可以在分配该变量的线程将其释放后尝试访问该变量。因此,Java将对自由本地变量的访问实现为对其副本的访问,而不是对原始变量的访问。如果仅将局部变量分配一次,则没有任何区别,因此有限制。其次,这种限制也不利于典型的命令式编程模式(正如我们在后面的章节中所解释的那样,


1
我真的认为此时Java 8 in Action中存在一些问题。如果此处的局部变量是指在方法中创建但由lambda访问的变量,并且通过实现了多线程ForkJoin,则将存在不同线程的副本,并且在理论上可以接受lambda中的突变,在这种情况下,它可以被变异。但是这里的情况有所不同,lambda中使用的局部变量用于通过实现的并行化parallelStream,并且这些局部变量由基于lambda的不同线程共享。
聆听

因此,第一点实际上是不对的,没有所谓的copy,它在parallelStream中的线程之间共享。就像第二点一样,在线程之间共享可变变量也很危险。这就是为什么我们阻止它并在Stream中引入内置方法来处理这些情况的原因。
聆听

14

因为实例变量总是通过对某个对象(即)的引用的字段访问操作来访问some_expression.instance_variable。即使您没有通过点符号(如)显式访问它instance_variable,也将其隐式视为this.instance_variable(或如果您在内部类中访问外部类的实例变量OuterClass.this.instance_variable,该变量位于内部this.<hidden reference to outer this>.instance_variable)。

因此,永远不会直接访问实例变量,而您直接访问的真正“变量”是this(实际上是最终的,因为它是不可赋值的),或者是其他表达式开头的变量。


这个问题的好解释
Sritam Jagadev

12

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

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

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

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

在此处输入图片说明

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


2
您介意提供Oracle幻灯片的源代码吗?
flow2k

@hagrawal您能否详细说明有关多线程环境的最终声明?因为许多线程正在同时运行,所以它是否在任何时候都与成员变量的实际值有关,因此它们可以覆盖实例变量。另外,如果我正确同步成员变量,那么问题仍然存在吗?
Yug Singh,

1
我猜出这个问题的最佳答案;)
Supun Wijerathne '18

9

似乎您在询问可以从lambda主体引用的变量。

根据JLS§15.27.2

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

因此,您无需声明变量,final只需确保它们是“有效的最终变量”即可。这与应用于匿名类的规则相同。


3
是的,但是实例变量可以在lambda中引用和分配,这令我感到惊讶。仅局部变量具有final限制。
杰拉德2014年

@Gerard因为实例变量具有整个类的范围。这与匿名类的逻辑完全相同-有很多教程对此逻辑进行了解释。
蜘蛛鲍里斯(Boris the Spider)

6

在Lambda表达式中,您可以有效地使用周围范围的最终变量。有效地表示,不必强制将变量声明为final,但请确保不要在lambda表达式内更改其状态。

您还可以在闭包中使用此函数,使用“ this”表示封装对象,而不是lambda本身,因为闭包是匿名函数,并且它们没有与之关联的类。

因此,当您使用封闭类中未声明为final且未有效地final的任何字段(让我们说私有Integer i;)时,它仍然可以工作,因为编译器代表您进行欺骗并插入“ this”(this.i) 。

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

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

 }

输出:Odd = true


4

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

如上所述的东西:

    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; 
    }

限制局部变量的基本原理是关于数据和计算的有效性

如果由第二个线程评估的lambda具有突变局部变量的能力。甚至从另一个线程读取可变局部变量的值的能力也将引入同步或使用volatile的必要性,以避免读取过时的数据。

但据我们所知,lambda主要目的

在各种不同的原因中,Java平台最紧迫的原因是,它们使在多个线程上分布集合的处理变得更加容易。

与局部变量完全不同,局部实例可以被突变,因为它是全局共享的。我们可以通过堆和栈的区别更好地理解这一点:

每当创建对象时,它始终存储在堆空间中,并且堆栈存储器包含对该对象的引用。堆栈内存仅包含局部基本变量和堆空间中对象的引用变量。

综上所述,我认为有两点很重要:

  1. 要使实例 有效地终结是非常困难的,这可能会导致很多毫无意义的负担(想像一下嵌套很深的类);

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

这里的平衡点很明确:如果您知道自己在做什么,就可以轻松做到,但是如果不知道,那么默认限制将有助于避免隐患

PS如果实例变异中需要同步,则可以直接使用流减少方法,或者实例变异中存在依赖性问题,您仍然可以在或Function while或方法中使用或。thenApplythenComposemapping


0

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.