3 回答
TA贡献1875条经验 获得超3个赞
Lambda 表达式的工作方式类似于回调。在将它们传递到代码中的那一刻,它们“存储”了它们操作所需的任何外部值(或引用)(就好像这些值在函数调用中作为参数传递一样。这只是对开发人员隐藏)。k在您的第一个示例中,您可以通过存储到单独的变量(如 d)来解决此问题:
for (int k = 0; k < 10; k++) {
final int d = k
new Thread(() -> System.out.println(d)).start();
}
实际上final意味着,在上面的示例中,您可以省略“final”关键字,因为d它实际上是最终的,因为它在其范围内从未更改。
for循环的操作方式不同。它们是迭代代码(与回调相反)。它们在各自的范围内工作,并且可以使用自己堆栈上的所有变量。这意味着,for循环的代码块是外部代码块的一部分。
至于您突出显示的问题:
增强的for循环不能使用常规索引计数器操作,至少不能直接操作。增强for的循环(在非数组上)创建一个隐藏的迭代器。您可以通过以下方式对此进行测试:
Collection<String> mySet = new HashSet<>();
mySet.addAll(Arrays.asList("A", "B", "C"));
for (String myString : mySet) {
if (myString.equals("B")) {
mySet.remove(myString);
}
}
上面的示例将导致 ConcurrentModificationException。这是由于迭代器注意到底层集合在执行期间发生了变化。但是,在您的示例中,外部循环创建了一个“有效的最终”变量arg,可以在 lambda 表达式中引用,因为该值是在执行时捕获的。
防止捕获“非有效最终”值或多或少只是 Java 中的一种预防措施,因为在其他语言(例如 JavaScript)中,这会有所不同。
所以编译器理论上可以翻译你的代码,捕获值,然后继续,但它必须以不同的方式存储那个值,你可能会得到意想不到的结果。因此,为 Java 8 开发 lambdas 的团队正确地排除了这种情况,通过异常阻止它。
如果您需要更改 lambda 表达式中的外部变量的值,您可以声明一个单元素数组:
String[] myStringRef = { "before" };
someCallingMethod(() -> myStringRef[0] = "after" );
System.out.println(myStringRef[0]);
或使用AtomicReference<T>使其成为线程安全的。但是,对于您的示例,这可能会返回“之前”,因为回调很可能在 println 执行之后执行。
TA贡献1887条经验 获得超5个赞
在增强的 for 循环中,每次迭代都会初始化变量。来自Java 语言规范(JLS)的§14.14.2 :
...
当执行增强for语句时,局部变量在循环的每次迭代中被初始化为数组的连续元素或Iterable由表达式产生。增强语句的确切含义for通过翻译成基本for语句给出,如下:
如果Expression的类型是 的子类型Iterable,则翻译如下。
如果Expression的类型是Iterable<X>某个类型参数的子类型X,则令I为类型java.util.Iterator<X>;否则,I设为原始类型java.util.Iterator。
增强for语句等价于for以下形式的基本语句:
for (I #i = Expression.iterator(); #i.hasNext(); ) {
{VariableModifier} TargetType Identifier =
(TargetType) #i.next();
Statement
}
...
否则,表达式必须具有数组类型,T[]。
让L1 ... Lm是紧接在增强for语句之前的(可能为空的)标签序列。
增强for语句等价于for以下形式的基本语句:
T[] #a = Expression;
L1: L2: ... Lm:
for (int #i = 0; #i < #a.length; #i++) {
{VariableModifier} TargetType Identifier = #a[#i];
Statement
}
...
换句话说,您的增强 for 循环等效于:
ArrayList<Integer> listOfInt = new ArrayList<>();
// add elements...
for (Iterator<Integer> itr = listOfInt.iterator(); itr.hasNext(); ) {
Integer arg = itr.next();
new Thread(() -> System.out.println(arg)).start();
}
由于每次迭代都会初始化变量,因此它实际上是最终的(除非您在循环内修改变量)。
相反,基本 for 循环中的变量(k在您的情况下)被初始化一次并在每次迭代时更新(如果存在“ ForUpdate ”,例如k++)。有关详细信息,请参阅JLS 的§14.14.1。由于变量更新,每次迭代都不是最终的,也不是有效的最终。
JLS 的§15.27.2规定并解释了对最终或有效最终变量的需求:
...
任何使用但未在 lambda 表达式中声明的局部变量、形式参数或异常参数都必须声明final或有效地最终确定(第 4.12.4 节),否则在尝试使用时会发生编译时错误。
任何使用但未在 lambda 主体中声明的局部变量必须在 lambda 主体之前明确分配(第 16 节(Definite Assignment)),否则会发生编译时错误。
变量使用的类似规则适用于内部类的主体(第 8.1.3 节)。对有效最终变量的限制禁止访问动态变化的局部变量,这些变量的捕获可能会引入并发问题。与final限制相比,它减轻了程序员的文书负担。
对有效最终变量的限制包括标准循环变量,但不包括增强for循环变量,它们对于循环的每次迭代都被视为不同的(第 14.14.2 节)。
...
最后一句话甚至明确提到了基本 for 循环变量和增强型 for 循环变量之间的区别。
TA贡献1853条经验 获得超18个赞
其他回复很有帮助,但他们似乎没有直接解决问题并明确回答。
在您的第一个示例中,您尝试k从 lambda 表达式进行访问。这里的问题是k随着时间的推移改变它的值(k++在每次循环迭代之后调用)。Lambda 表达式确实捕获了外部引用,但它们需要被标记为final或“有效地最终”(即,将它们标记为final仍会产生有效代码)。这是为了防止并发问题;在您创建的线程运行时,k可能已经拥有一个新值。
另一方面,在您的第二个示例中,您正在访问的变量是arg,它会在增强的 for 循环的每次迭代中重新初始化(与上面的示例相比,k仅更新),因此您正在创建一个完全每次迭代的新变量。顺便说一句,您还可以将增强型 for 循环的迭代变量显式声明为final:
for (final Integer arg : listOfInt) {
new Thread(() -> System.out.println(arg)).start();
}
这可确保在arg您创建的线程运行时值引用不会更改。
添加回答
举报