据我了解,lambda表达式捕获值,而不是变量。例如,以下是编译时错误:
for (int k = 0; k < 10; k++) {
new Thread(() -> System.out.println(k)).start();
// Error—cannot capture k
// Local variable k defined in an enclosing scope must be final or effectively final
}
但是,当我尝试使用增强的for-loop
运行相同的逻辑时,一切运行正常:
List<Integer> listOfInt = new Arrays.asList(1, 2, 3);
for (Integer arg : listOfInt) {
new Thread(() -> System.out.println(arg)).start();
// OK to capture 'arg'
}
为什么增强的for
循环也可以像普通循环那样在变量内部递增,但为什么对于增强的for
循环却不能正常工作,为什么? / p>
答案 0 :(得分:30)
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
循环(在非数组上)创建一个隐藏的Iterator。您可以通过以下方式进行测试:
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的lambda的团队通过排除异常来正确排除了这种情况。
如果您需要在lambda表达式中更改外部变量的值,则可以声明一个单元素数组:
String[] myStringRef = { "before" };
someCallingMethod(() -> myStringRef[0] = "after" );
System.out.println(myStringRef[0]);
或使用Atomic
答案 1 :(得分:13)
在增强的for循环中,变量在每次迭代时都会初始化。摘自 Java语言规范(JLS)的§14.14.2:
...
执行增强的
for
语句时,在循环的每次迭代中,将局部变量初始化为数组或表达式生成的Iterable
的连续元素。增强的for
语句的确切含义是通过翻译成基本的for
语句来给出的,如下所示:
如果 Expression 的类型是
Iterable
的子类型,则翻译如下。如果对于某些类型参数
Iterable<X>
, Expression 的类型是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
)中的变量在每次迭代时均被初始化 和 updated (如果为“ 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循环变量之间的区别。
答案 2 :(得分:3)
其他答复是有帮助的,但它们似乎并不能直接解决问题并以明确的方式回答。
在第一个示例中,您尝试从lambda表达式访问k
。这里的问题是k
会随着时间改变其值(k++
在每次循环迭代后都会被调用)。 Lambda表达式确实捕获了外部引用,但是需要将它们标记为final
或“有效地最终定稿”(即,将它们标记为final
仍会产生有效的代码)。这是为了防止并发问题;在您创建的线程运行时,k
可能已经拥有一个新值。
另一方面,在第二个示例中,您要访问的变量是arg
,它在增强型for循环的每次迭代中都被重新初始化(与示例比较)上面的k
仅被更新了),因此您每次迭代都会创建一个全新的变量。顺便说一句,您还可以将增强型for循环的迭代变量明确声明为final
:
for (final Integer arg : listOfInt) {
new Thread(() -> System.out.println(arg)).start();
}
这可确保在您创建的线程运行时,值arg
引用不会更改。
答案 3 :(得分:1)
defined to be equivalent是此代码的增强型for
循环:
for (Iterator<T> it = iterable.iterator(); it.hasNext(); ) {
T loopvar = it.next();
…
}
此替换代码说明了为什么将增强的for
循环的变量视为有效最终的。