在 Java 中关闭 - 捕获的值 - 为什么会出现这种意外结果?
Closure in Java - Captured value - why this unexpected result?
我想我在Java脚本中遇到过这种经典情况。
通常程序员希望下面的代码打印"Peter"、"Paul"、"Mary"。
但事实并非如此。谁能准确解释为什么它在 Java 中以这种方式工作?
此Java 8 段代码编译成功并打印3 次"Mary"。
我想这与它如何深入实施有关
但是......这是否表明错误的底层实现?
import java.util.List;
import java.util.ArrayList;
public class Test008 {
public static void main(String[] args) {
String[] names = { "Peter", "Paul", "Mary" };
List<Runnable> runners = new ArrayList<>();
int[] ind = {0};
for (int i = 0; i < names.length; i++){
ind[0] = i;
runners.add(() -> System.out.println(names[ind[0]]));
}
for (int k=0; k<runners.size(); k++){
runners.get(k).run();
}
}
}
相反,如果我使用增强的 for 循环(同时添加 Runnable),则会捕获正确的(即所有不同的)值。
for (String name : names){
runners.add(() -> System.out.println(name));
}
最后,如果我使用经典的 for 循环(同时添加 Runnable),则会出现编译错误(这很合理,因为变量 i
不是最终的或实际上不是最终的)。
for (int i = 0; i < names.length; i++){
runners.add(() -> System.out.println(names[i]));
}
编辑:
我的观点是:为什么没有捕获 names[ind[0]]
的值(我添加 Runnable 时它所具有的值)?我什么时候执行 lambda 表达式应该无关紧要,对吧?我的意思是,好的,在具有增强 for 循环的版本中,我稍后也会执行 Runnables,但 correct/distinct 值是较早捕获的(在添加 Runnables 时)。
换句话说,为什么在捕获值时 Java 不总是具有这种 by value / snapshot 语义(如果我可以这样说的话)?它不会更干净,更有意义吗?
当您创建 lambda 表达式时,您并没有执行它。
它仅在您的第二个 for 循环中执行,当您调用每个 Runnable
.
的 run
方法时
执行第二个循环时,ind[0]
包含2,因此run
所有方法执行时打印"Mary"。
编辑:
增强的 for 循环的行为不同,因为在该片段中,lambda 表达式持有对 String
实例的引用,而 String
是不可变的。如果将 String
更改为 StringBuilder
,则可以使用增强的 for 循环构建一个示例,该循环还打印被引用实例的最终值:
StringBuilder[] names = { new StringBuilder().append ("Peter"), new StringBuilder().append ("Paul"), new StringBuilder().append ("Mary") };
List<Runnable> runners = new ArrayList<>();
for (StringBuilder name : names){
runners.add(() -> System.out.println(name));
name.setLength (0);
name.append ("Mary");
}
for (int k=0; k<runners.size(); k++){
runners.get(k).run();
}
输出:
Mary
Mary
Mary
我会说代码完全按照你告诉他的去做。
您创建 "Runnable",它在 ind[0]
的位置打印名称。
但是该表达式在您的第二个 for 循环中得到评估。而此时ind[0]=2
。所以表达式打印 "Mary"
。
它工作得很好.. 在调用 println
方法时,ind[0]
是 2
,因为您正在使用一个公共变量并在函数调用之前增加它的值。因此它将始终打印 Mary
。您可以执行以下操作来检查
for (int i = 0; i < names.length; i++) {
final int j = i;
runners.add(() -> System.out.println(names[j]));
}
这将根据需要打印所有名称
或在本地声明ind
for (int i = 0; i < names.length; i++) {
final int[] ind = { 0 };
ind[0] = i;
runners.add(() -> System.out.println(names[ind[0]]));
}
In other words, why does not Java always have this by value / snapshot semantics (if I may put it this way) when capturing values? Wouldn't it be cleaner and make more sense?
Java lambdas do 具有按值捕获语义。
当你这样做时:
runners.add(() -> System.out.println(names[ind[0]]));
此处的 lambda 捕获两个 值:ind
和 names
。这两个值恰好是对象引用,但对象引用是一个类似于“3”的值。当捕获的对象引用引用可变对象时,事情就会变得混乱,因为它们在 lambda 捕获期间的状态和它们在 lambda 调用期间的状态可能不同。具体来说, ind
所指的数组确实以这种方式发生变化,这就是你的问题的原因。
lambda 不捕获的是表达式 ind[0]
的值。相反,它捕获引用 ind
,并且当 lambda 被 调用时 ,执行取消引用 ind[0]
。 Lambdas 从它们的词法封闭范围中关闭值; ind
是词法封闭范围内的值,但 ind[0]
不是。我们捕获 ind
并在评估时进一步使用它。
您在这里以某种方式期望 lambda 将对整个堆中可通过捕获的引用访问的所有对象进行完整快照,但这不是它的工作方式——也没有多大意义。
总结:Lambda 按值捕获所有捕获的参数——包括对象引用。但是对象引用和它所引用的对象不是一回事。如果您捕获对可变对象的引用,则该对象的状态可能在调用 lambda 时已更改。
好吧,在 lambda 之前,Java 曾经有更严格的规则来捕获匿名 local/parameter 变量 类。即只允许捕获final
个变量。我相信这是为了防止并发混淆——最终变量不会改变。因此,如果您将以这种方式创建的一些实例传递到任务队列中,并让另一个线程执行它,至少您知道这两个线程共享相同的值。 Why are only final variables accessible in anonymous class?
解释了更深层次的原因
现在,使用 lambda 和 Java 8,您可以捕获 "effectively final" 的变量,这些变量未声明 [=10=] 但被编译器认为是最终变量到它是如何读和写的。基本上,如果您将 final 关键字添加到有效 final 变量的声明中,编译器就不会抱怨。
解释此行为的另一种方法是查看 lambda 表达式替换的内容:
runners.add(() -> System.out.println(names[ind[0]]));
是
的语法糖
runers.add(new Runnable() {
final String[] names_inner = names;
final int[] ind_inner = ind;
public void run() {
System.out.println(names_inner[ind_inner[0]]));
}
});
变成
// yes, this is inside the main method's body
class Test008 implements Runnable {
final String[] names_inner;
final int[] ind_inner;
private Test008(String[] n, int[] i) {
names_inner = n; ind_inner = i;
}
public void run() {
System.out.println(names_inner[ind_inner[0]]));
}
}
runers.add(new Test008(names,ind));
(生成的东西的名称并不重要。那个美元符号只是在 Java 中生成的 methods/classes 的名称中经常使用的字符 - 这就是它被保留的原因。
)
增加了两个内部字段,使得Runnable里面的代码可以看到外面的变量;如果 names
和 ind
在外部代码(main 方法)中定义为 final,则内部字段将不是必需的。这样变量就可以按值传递,正如 Brian Goetz 在他的回答中解释的那样。
数组就像对象,如果将它们传递给修改它们的方法,它们也会在原始位置被修改;剩下的只是 OOP 基础知识。所以这实际上是正确的行为。
我想我在Java脚本中遇到过这种经典情况。
通常程序员希望下面的代码打印"Peter"、"Paul"、"Mary"。
但事实并非如此。谁能准确解释为什么它在 Java 中以这种方式工作?
此Java 8 段代码编译成功并打印3 次"Mary"。
我想这与它如何深入实施有关
但是......这是否表明错误的底层实现?
import java.util.List;
import java.util.ArrayList;
public class Test008 {
public static void main(String[] args) {
String[] names = { "Peter", "Paul", "Mary" };
List<Runnable> runners = new ArrayList<>();
int[] ind = {0};
for (int i = 0; i < names.length; i++){
ind[0] = i;
runners.add(() -> System.out.println(names[ind[0]]));
}
for (int k=0; k<runners.size(); k++){
runners.get(k).run();
}
}
}
相反,如果我使用增强的 for 循环(同时添加 Runnable),则会捕获正确的(即所有不同的)值。
for (String name : names){
runners.add(() -> System.out.println(name));
}
最后,如果我使用经典的 for 循环(同时添加 Runnable),则会出现编译错误(这很合理,因为变量 i
不是最终的或实际上不是最终的)。
for (int i = 0; i < names.length; i++){
runners.add(() -> System.out.println(names[i]));
}
编辑:
我的观点是:为什么没有捕获 names[ind[0]]
的值(我添加 Runnable 时它所具有的值)?我什么时候执行 lambda 表达式应该无关紧要,对吧?我的意思是,好的,在具有增强 for 循环的版本中,我稍后也会执行 Runnables,但 correct/distinct 值是较早捕获的(在添加 Runnables 时)。
换句话说,为什么在捕获值时 Java 不总是具有这种 by value / snapshot 语义(如果我可以这样说的话)?它不会更干净,更有意义吗?
当您创建 lambda 表达式时,您并没有执行它。
它仅在您的第二个 for 循环中执行,当您调用每个 Runnable
.
run
方法时
执行第二个循环时,ind[0]
包含2,因此run
所有方法执行时打印"Mary"。
编辑:
增强的 for 循环的行为不同,因为在该片段中,lambda 表达式持有对 String
实例的引用,而 String
是不可变的。如果将 String
更改为 StringBuilder
,则可以使用增强的 for 循环构建一个示例,该循环还打印被引用实例的最终值:
StringBuilder[] names = { new StringBuilder().append ("Peter"), new StringBuilder().append ("Paul"), new StringBuilder().append ("Mary") };
List<Runnable> runners = new ArrayList<>();
for (StringBuilder name : names){
runners.add(() -> System.out.println(name));
name.setLength (0);
name.append ("Mary");
}
for (int k=0; k<runners.size(); k++){
runners.get(k).run();
}
输出:
Mary
Mary
Mary
我会说代码完全按照你告诉他的去做。
您创建 "Runnable",它在 ind[0]
的位置打印名称。
但是该表达式在您的第二个 for 循环中得到评估。而此时ind[0]=2
。所以表达式打印 "Mary"
。
它工作得很好.. 在调用 println
方法时,ind[0]
是 2
,因为您正在使用一个公共变量并在函数调用之前增加它的值。因此它将始终打印 Mary
。您可以执行以下操作来检查
for (int i = 0; i < names.length; i++) {
final int j = i;
runners.add(() -> System.out.println(names[j]));
}
这将根据需要打印所有名称
或在本地声明ind
for (int i = 0; i < names.length; i++) {
final int[] ind = { 0 };
ind[0] = i;
runners.add(() -> System.out.println(names[ind[0]]));
}
In other words, why does not Java always have this by value / snapshot semantics (if I may put it this way) when capturing values? Wouldn't it be cleaner and make more sense?
Java lambdas do 具有按值捕获语义。
当你这样做时:
runners.add(() -> System.out.println(names[ind[0]]));
此处的 lambda 捕获两个 值:ind
和 names
。这两个值恰好是对象引用,但对象引用是一个类似于“3”的值。当捕获的对象引用引用可变对象时,事情就会变得混乱,因为它们在 lambda 捕获期间的状态和它们在 lambda 调用期间的状态可能不同。具体来说, ind
所指的数组确实以这种方式发生变化,这就是你的问题的原因。
lambda 不捕获的是表达式 ind[0]
的值。相反,它捕获引用 ind
,并且当 lambda 被 调用时 ,执行取消引用 ind[0]
。 Lambdas 从它们的词法封闭范围中关闭值; ind
是词法封闭范围内的值,但 ind[0]
不是。我们捕获 ind
并在评估时进一步使用它。
您在这里以某种方式期望 lambda 将对整个堆中可通过捕获的引用访问的所有对象进行完整快照,但这不是它的工作方式——也没有多大意义。
总结:Lambda 按值捕获所有捕获的参数——包括对象引用。但是对象引用和它所引用的对象不是一回事。如果您捕获对可变对象的引用,则该对象的状态可能在调用 lambda 时已更改。
好吧,在 lambda 之前,Java 曾经有更严格的规则来捕获匿名 local/parameter 变量 类。即只允许捕获final
个变量。我相信这是为了防止并发混淆——最终变量不会改变。因此,如果您将以这种方式创建的一些实例传递到任务队列中,并让另一个线程执行它,至少您知道这两个线程共享相同的值。 Why are only final variables accessible in anonymous class?
现在,使用 lambda 和 Java 8,您可以捕获 "effectively final" 的变量,这些变量未声明 [=10=] 但被编译器认为是最终变量到它是如何读和写的。基本上,如果您将 final 关键字添加到有效 final 变量的声明中,编译器就不会抱怨。
解释此行为的另一种方法是查看 lambda 表达式替换的内容:
runners.add(() -> System.out.println(names[ind[0]]));
是
的语法糖runers.add(new Runnable() {
final String[] names_inner = names;
final int[] ind_inner = ind;
public void run() {
System.out.println(names_inner[ind_inner[0]]));
}
});
变成
// yes, this is inside the main method's body
class Test008 implements Runnable {
final String[] names_inner;
final int[] ind_inner;
private Test008(String[] n, int[] i) {
names_inner = n; ind_inner = i;
}
public void run() {
System.out.println(names_inner[ind_inner[0]]));
}
}
runers.add(new Test008(names,ind));
(生成的东西的名称并不重要。那个美元符号只是在 Java 中生成的 methods/classes 的名称中经常使用的字符 - 这就是它被保留的原因。 )
增加了两个内部字段,使得Runnable里面的代码可以看到外面的变量;如果 names
和 ind
在外部代码(main 方法)中定义为 final,则内部字段将不是必需的。这样变量就可以按值传递,正如 Brian Goetz 在他的回答中解释的那样。
数组就像对象,如果将它们传递给修改它们的方法,它们也会在原始位置被修改;剩下的只是 OOP 基础知识。所以这实际上是正确的行为。