改变Lambda表达式的"自由变量"

Sta*_*lfi 4 java final java-8

我读大约Lambda表达式梦幻般的文章,以下是未清除的对我说:

  1. Lambda Expression是否将自由变量或refernse /指针的值保存到每个变量中?(我猜答案是后者,因为如果没有,mutate自由变量将是有效的).

不要指望编译器捕获所有并发访问错误.禁止变异只适用于局部变量.

我不确定自我实验会涵盖所有情况,所以我正在寻找一个明确定义的规则:

  1. 哪些自由变量可以在Lambda表达式中变异(静态/属性/局部变量/参数),哪些可以在Lambda表达式中使用时突变?
  2. 在我使用它(读取或调用他的一个方法)之后,我可以在Lambda表达式块结束后改变每个自由变量吗?

不要指望编译器捕获所有并发访问错误.禁止变异只适用于局部变量.如果matches是封闭类的实例或静态变量,则不会报告错误,即使结果与未定义相同.

  1. 即使我使用同步算法,突变的结果是否未定义?

更新1:

自由变量 - 即不是参数且未在代码中定义的变量.

简单来说,我可以得出结论,自由变量是不是Lambda表达式参数的所有变量,并且没有在同一个Lambda表达式中定义?

Hol*_*ger 6

你的术语"自由变量"充其量是误导性的.如果你不是在谈论局部变量(必须有效地最终被捕获),那么你所说的是堆变量.

堆变量可以是实例字段,static字段或数组元素.为了从周围的上下文中无条件地访问实例变量,lambda表达式可以(并将)通过捕获的this引用访问它们.对于其他实例字段以及数组元素,无论如何都需要通过变量进行显式访问,因此很清楚,如何访问堆变量.只能static直接访问字段.

规则很简单,除非声明,否则final可以在lambda表达式的内部或外部修改所有规则.请记住,lambda表达式可以调用任意方法,无论如何都包含任意代码.这是否会导致问题,取决于您如何使用lambda表达式.您甚至可以创建不直接修改变量的函数的问题,而不需要任何并发性,例如

ArrayList<String> list=new ArrayList<>(Arrays.asList("foo", "bar"));
list.removeIf(s -> list.remove("bar"));
Run Code Online (Sandbox Code Playgroud)

java.util.ConcurrentModificationException由于正在进行的迭代中的列表修改,可能会抛出一个.

同样,即使您确保以线程安全的方式完成对变量本身的修改,修改并发上下文中的变量或资源也可能会破坏它.这都是关于您正在使用的API的合同.

最值得注意的是,当使用并行Streams时,您必须意识到函数不仅由不同的线程进行评估,它们还在评估Stream的任意元素,而不管它们的遭遇顺序如何.对于Stream处理的最终结果,实现将以重新建立遭遇顺序的方式组合部分结果,如果需要,但是中间操作以任意顺序评估元素,因此您的函数不仅必须是线程安全的,而且也不依赖于特定的处理订单.在某些情况下,他们甚至可能处理对最终结果没有贡献的元素.

由于你的子弹3指的是"在一个块结束之后",我想强调一下,在你的lambda表达式中的哪个位置发生修改(或可感知的副作用)是无关紧要的.

一般来说,你最好使用没有这种副作用的功能.但这并不意味着他们一般都被禁止.


Eug*_*ene 6

对于更简单的主题,这看起来像复杂的"单词".规则与匿名类几乎相同.

例如,编译器捕获了这个:

 int x = 3;

 Runnable r = () -> {
    x = 6; // Local variable x defined in an enclosing scope must be final or effectively final
 };
Run Code Online (Sandbox Code Playgroud)

但同时这样做是完全合法的(从编译器的角度来看):

    final int x[] = { 0 };

    Runnable r = () -> {
        x[0] = 6;
    };
Run Code Online (Sandbox Code Playgroud)

您提供并使用的示例matches:

 List<Path> matches = new ArrayList<>();
    List<Path> files = List.of();
    for (Path p : files) {
        new Thread(() -> {
            if (1 == 1) {
                matches.add(p);
            }
        }).start();
    }
Run Code Online (Sandbox Code Playgroud)

有同样的问题.编译器不会抱怨你编辑匹配(因为你没有改变引用matches- 所以它是effectively final); 但同时这可以undefined results.side-effects一般而言,此操作已经并且不鼓励.未定义的结果将来自于您显然matches不是一个thread-safe集合的事实.

你最后一点:Does the result of the mutation is undefined even when I use a synchroniziton algorithm?.当然不是.通过适当的同步更新,变量outsidelambda(或流)将起作用 - 但是不鼓励,主要是因为还有其他方法可以实现.

编辑

好的,所以自由变量是那些未在lambda代码本身内定义的变量,或者不是 lambda本身的参数.

在这种情况下,1)的答案是:lambda表达式对方法去糖,规则free-variables与匿名类相同.这已经多次讨论过了,就像这里一样.这实际上也回答了第二个问题 - 因为规则是相同的.显然任何final or effectively final可以变异的东西.对于原语 - 这意味着它们不能被变异; 对于对象,您不能改变引用(但可以更改基础数据 - 如我的示例所示).对于3) - 是的.

  • 术语"未定义"本身并没有很好地定义.正如我在回答中所解释的那样,您可以使用变量更新来保证线程安全,因此您的结果不像"C++未定义行为"那样未定义,但仍然存在不可预测的错误结果.因此,对于您的示例,将`matches`更改为使用`Vector`而不是`ArrayList`会使修改线程安全,但是,在没有可预测的顺序且没有明确等待完成的意义上,结果将是未定义的,你甚至不能假设看到所有匹配的元素. (4认同)