字段和局部变量之间的根本区别在于,当JVM创建lambda实例时,局部变量被复制。另一方面,可以自由更改字段,因为对它们的更改也会传播到外部类实例(它们的作用域是整个外部类,如Boris在下面指出的那样)。
匿名类、闭包和lambda最容易从变量作用域的角度来思考;想象一下,为所有传递给闭包的局部变量添加了一个复制构造函数。
在项目lambda的文档中,Lambda状态v4的第7节“变量捕获”中提到:
我们的意图是禁止捕获可变局部变量。原因是这种习惯用法:
int sum = 0; list.forEach(e -> { sum += e.size(); });
本质上,Lambda表达式是串行的;编写此类没有竞争条件的Lambda体相当困难。除非我们愿意在编译时强制执行不能逃出其捕获线程的函数,否则此功能可能会带来更多麻烦。
另一个需要注意的问题是,当您在内部类中访问局部变量时,这些局部变量在内部类的构造函数中被传递。对于非final变量,这种方式无法使用,因为非final变量的值可以在构造后更改。
而对于实例变量,编译器将传递对象的引用,并且对象引用将用于访问实例变量。因此,在实例变量的情况下不需要进行此操作。
PS:值得一提的是,在Java SE7中,匿名类只能访问final局部变量;而在Java SE8中,Lambda以及内部类也可以访问有效的final变量。
ForkJoin
实现的,那么不同线程将有一个副本,理论上允许在lambda中进行突变,这种情况下它可以被“突变”。但这里的情况不同,lambda中使用的“局部变量”是通过parallelStream
实现的并行化,这些“局部变量”由基于lambda的不同线程共享。 - Hearen为未来的访问者提供一些概念:
总之,编译器应该能够确定地告诉我们,lambda表达式体不会对变量的陈旧副本进行操作。
对于局部变量,在没有将变量声明为final或effectively final的情况下,编译器无法确定lambda表达式体是否在使用变量的陈旧副本,因此局部变量应该是final或effectively final。
现在,对于实例字段,当您在lambda表达式中访问实例字段时,编译器将在变量访问中附加一个this
(如果您没有明确执行),由于this
是 effectively final,因此编译器可以确保lambda表达式体始终具有最新的变量副本(请注意,多线程不在此讨论范围内)。所以,对于实例字段,编译器可以确定lambda表达式体具有实例变量的最新副本,因此实例变量无需是final或effectively final。请参考以下来自Oracle幻灯片的屏幕截图:
另外,请注意,如果您在lambda表达式中访问实例字段并且该表达式在多线程环境中运行,则可能会遇到问题。
some_expression.instance_variable
。 即使您没有通过点符号显式地访问它,例如instance_variable
,也会被隐式视为this.instance_variable
(如果您在内部类中访问外部类的实例变量,则为OuterClass.this.instance_variable
,在幕后是this.<hidden reference to outer this>.instance_variable
)。this
(它是“有效地final”,因为它不可分配),或者是某个其他表达式开头的变量。看起来您询问的是lambda主体可以引用的变量。
根据JLS §15.27.2:
在lambda表达式中使用但未声明的任何局部变量、形式参数或异常参数必须声明为final或实际上为final(§4.12.4),否则在尝试使用时将导致编译时错误。
因此,您无需将变量声明为final
,只需要确保它们是“实际上为final”的即可。这与匿名类适用相同的规则。
在Lambda表达式中,您可以有效地使用周围作用域的变量。有效意味着不必声明变量为final,但请确保您不会在lambda表达式中更改其状态。
您还可以在闭包中使用此功能,并使用“this”表示封闭对象,但不表示Lambda本身,因为闭包是匿名函数,没有与其关联的类。
因此,当您使用封闭类中的任何字段(比如说private Integer i;),该字段未被声明为final并且不是有效final时,仍将起作用,因为编译器代表您执行了技巧并插入了“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();
}
}
输出: 奇数=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以避免读取陈旧数据的必要性。
但我们知道lambda表达式的主要目的
在 Java 平台上,它们使将集合的处理分布到多个线程上更容易。
与局部变量不同,局部实例可以被改变,因为它在全局范围内是共享的。我们可以通过堆栈差异更好地理解这一点:
每当创建一个对象时,它总是存储在堆空间中,而堆栈内存包含对它的引用。堆栈内存仅包含局部原始变量和指向堆空间中对象的引用变量。
因此,总结起来,有两点我认为非常重要:
很难使实例有效地成为最终,这可能会带来很多无意义的负担(想象一下深度嵌套的类);
实例本身已经在全局范围内共享,lambda也可以在线程之间共享,因此它们可以正确地协同工作,因为我们知道我们正在处理突变并希望传递这种突变;
平衡点很清楚:如果你知道自己在做什么,就可以轻松地完成,但如果不知道,则默认限制将有助于避免隐蔽的错误。
P.S. 如果实例变异需要同步,则可以直接使用流规约方法,如果实例变异存在依赖问题,则仍然可以在Function中使用thenApply
或thenCompose
进行mapping
或类似的方法。
首先,本地变量和实例变量在幕后的实现方式有关键区别。实例变量存储在堆中,而本地变量存储在栈中。 如果 Lambda 直接访问本地变量并且 Lambda 在线程中使用,则使用 Lambda 的线程可能会尝试在分配变量的线程释放变量后访问该变量。
简而言之:为了确保另一个线程不会覆盖原始值,最好提供对副本变量的访问权限,而不是原始变量。
Integer[] count = {new Integer(5)}
。另请参见https://dev59.com/Q1UL5IYBdhLWcg3wYHDD#50457016。 - flow2k