在lambda中,局部变量需要是最终变量,而实例变量则不需要。为什么这样?
Integer[] count = {new Integer(5)}
。另请参阅stackoverflow.com/a/50457016/7154924。
在lambda中,局部变量需要是最终变量,而实例变量则不需要。为什么这样?
Integer[] count = {new Integer(5)}
。另请参阅stackoverflow.com/a/50457016/7154924。
Answers:
字段和局部变量之间的根本区别在于,当JVM创建lambda实例时,将复制局部变量。另一方面,字段可以自由更改,因为对它们的更改也将传播到外部类实例(它们的范围是整个外部类,如Boris所指出的)。
考虑到匿名类,闭包和labmdas的最简单方法是从可变范围的角度;想象一个为传递给闭包的所有局部变量添加的副本构造函数。
在lambda项目的文档中:Lambda v4的状态
在第7节“变量捕获”下,提到了...。
我们的目的是禁止捕获可变的局部变量。原因是这样的成语:
int sum = 0; list.forEach(e -> { sum += e.size(); });
基本上是连续的;这样写没有竞争条件的lambda体非常困难。除非我们愿意(最好是在编译时)强制执行这样一个函数无法逃脱其捕获线程的功能,否则此功能可能会带来更多无法解决的麻烦。
编辑:
这里要注意的另一件事是,在内部类中访问局部变量时,它们会在内部类的构造函数中传递,这不适用于非最终变量,因为非最终变量的值可以在构造后更改。
在实例变量的情况下,编译器会传递类的引用,而类的引用将用于访问实例变量。因此,在实例变量的情况下不是必需的。
PS:值得一提的是,匿名类只能访问最终的局部变量(在JAVA SE 7中),而在Java SE 8中,您可以在lambda以及内部类中有效地访问最终变量。
在Java 8 in Action书中,这种情况解释为:
您可能会问自己,为什么局部变量具有这些限制。首先,幕后实现实例和局部变量的方式存在关键差异。实例变量存储在堆中,而局部变量存在于堆栈中。如果lambda可以直接访问局部变量并且该lambda在线程中使用,则使用lambda的线程可以在分配该变量的线程将其释放后尝试访问该变量。因此,Java将对自由本地变量的访问实现为对其副本的访问,而不是对原始变量的访问。如果仅将局部变量分配一次,则没有任何区别,因此有限制。其次,这种限制也不利于典型的命令式编程模式(正如我们在后面的章节中所解释的那样,
ForkJoin
,则将存在不同线程的副本,并且在理论上可以接受lambda中的突变,在这种情况下,它可以被变异。但是这里的情况有所不同,lambda中使用的局部变量用于通过实现的并行化parallelStream
,并且这些局部变量由基于lambda的不同线程共享。
因为实例变量总是通过对某个对象(即)的引用的字段访问操作来访问some_expression.instance_variable
。即使您没有通过点符号(如)显式访问它instance_variable
,也将其隐式视为this.instance_variable
(或如果您在内部类中访问外部类的实例变量OuterClass.this.instance_variable
,该变量位于内部this.<hidden reference to outer this>.instance_variable
)。
因此,永远不会直接访问实例变量,而您直接访问的真正“变量”是this
(实际上是最终的,因为它是不可赋值的),或者是其他表达式开头的变量。
为将来的访客提出一些概念:
基本上,一切都归结为编译器应该能够确定性地确定lambda表达式主体不在变量的陈旧副本上工作。
在使用局部变量的情况下,编译器无法确保lambda表达式主体不会在变量的陈旧副本上工作,除非该变量是final或实际上是final,所以局部变量应该是final或实际上是final。
现在,在使用实例字段的情况下,当您访问lambda表达式内的实例字段时,编译器将this
在变量访问后附加一个(如果您未显式地执行此操作),并且由于this
实际上是最终的,因此编译器确保lambda表达式主体将总是具有该变量的最新副本(请注意,此讨论现在不在多线程范围内)。因此,在实例实例字段的情况下,编译器可以告诉lambda主体具有实例变量的最新副本,因此实例变量不必是最终变量或有效地是最终变量。请参考以下Oracle幻灯片的屏幕截图:
另外,请注意,如果您正在访问lambda表达式中的实例字段,并且该实例字段正在多线程环境中执行,则可能会出现问题。
似乎您在询问可以从lambda主体引用的变量。
使用但未在lambda表达式中声明的任何局部变量,形式参数或异常参数必须声明为final或有效地声明为final(第4.12.4节),否则在尝试使用时会发生编译时错误。
因此,您无需声明变量,final
只需确保它们是“有效的最终变量”即可。这与应用于匿名类的规则相同。
final
限制。
在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);
}
这是一个代码示例,因为我也没想到这一点,所以我希望无法修改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
是的,您可以更改实例的成员变量,但不能像处理变量时一样更改实例本身。
如上所述的东西:
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的必要性,以避免读取过时的数据。
在各种不同的原因中,Java平台最紧迫的原因是,它们使在多个线程上分布集合的处理变得更加容易。
与局部变量完全不同,局部实例可以被突变,因为它是全局共享的。我们可以通过堆和栈的区别更好地理解这一点:
每当创建对象时,它始终存储在堆空间中,并且堆栈存储器包含对该对象的引用。堆栈内存仅包含局部基本变量和堆空间中对象的引用变量。
综上所述,我认为有两点很重要:
要使实例 有效地终结是非常困难的,这可能会导致很多毫无意义的负担(想像一下嵌套很深的类);
实例本身已经在全局范围内共享,并且lambda在线程之间也可以共享,因此它们可以正常工作,因为我们知道我们正在处理该突变,并且希望将这种突变传递出去;
这里的平衡点很明确:如果您知道自己在做什么,就可以轻松做到,但是如果不知道,那么默认限制将有助于避免隐患。
PS如果实例变异中需要同步,则可以直接使用流减少方法,或者实例变异中存在依赖性问题,您仍然可以在或Function while或方法中使用或。thenApply
thenCompose
mapping