增强型for循环和lambda表达式

40
据我理解,Lambda表达式捕获的是值而不是变量。例如,以下代码会在编译时报错:

To my understanding, lambda expressions capture values, not the variables. For example, the following is a compile-time error:


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循环运行相同的逻辑时,一切都正常:

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循环却不能,尽管增强for循环也会像常规循环一样逐步递增变量。
4个回答

38
Lambda表达式类似于回调函数,一旦它们在代码中被传递,它们会“存储”它们需要操作的任何外部值(或引用),就像这些值被传递为函数调用的参数一样(只是对开发人员隐藏)。在您的第一个示例中,您可以通过将k存储到一个单独的变量(如d)中来解决问题。
for (int k = 0; k < 10; k++) {
    final int d = k
    new Thread(() -> System.out.println(d)).start();
}

有效地说,以上例子中你可以省略'final'关键字,因为d在其作用域内从未改变,所以它被有效地视为final。

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异常。这是由于迭代器在执行过程中注意到底层集合已经发生了变化。然而,在你的示例中,外部循环创建了一个“有效地final”的变量arg,它可以在lambda表达式中引用,因为该值在执行时被捕获。
防止捕获“非有效final”值在Java中更多或少是一种预防措施,因为在其他语言(如JavaScript)中,这种情况有所不同。
因此,编译器理论上可以转换您的代码,捕获该值并继续执行,但它必须以不同的方式存储该值,您可能会得到意外的结果。因此,开发Java 8的lambda的团队正确地通过使用异常来防止此场景。
如果您需要在lambda表达式中更改外部变量的值,可以声明一个单元素数组:
String[] myStringRef = { "before" };
someCallingMethod(() -> myStringRef[0] = "after" );
System.out.println(myStringRef[0]);

或者使用AtomicReference<T>使其线程安全。然而,根据您的示例,这可能会返回“before”,因为回调很可能在println执行之后执行。


16
在增强型for循环中,变量会在每次迭代时被初始化。根据《Java语言规范》(JLS)的§14.14.2

...

执行增强型for语句时,局部变量会在循环的每次迭代中被初始化为表达式生成的数组或Iterable中的连续元素。增强型for语句的确切含义通过转换为基本for语句来给出,如下所示:

  • 如果Expression的类型是Iterable的子类型,则转换如下。

    如果Expression的类型是某个类型参数XIterable<X>的子类型,则让Ijava.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();
}

由于变量在每次迭代时都被初始化,因此它是有效 final的(除非你在循环内修改变量)。

相比之下,基本for循环中的变量(在您的情况下为k)仅被初始化一次,并且在每次迭代时进行更新(如果存在“ForUpdate”,例如k++)。有关更多信息,请参见 JLS 的§14.14.1。由于变量在每次迭代时被更新,因此它既不是final也不是有效 final。

需要一个final或有效final变量的原因和解释在JLS的§15.27.2中规定:

...

lambda表达式中使用但未声明的任何局部变量、形式参数或异常参数必须声明为final或有效final(§4.12.4),否则,在尝试使用时将出现编译时错误。

在lambda主体中使用但未声明的任何局部变量必须在lambda主体之前被明确定义(§16 (Definite Assignment)),否则将出现编译时错误。

类内(§8.1.3)也适用类似的变量使用规则。有效final变量的限制禁止访问动态更改的局部变量,这样的访问可能会引入并发问题。与final限制相比,它减轻了程序员的文书负担。

对有效final变量的限制包括标准循环变量,但不包括增强型for循环变量,后者在循环的每次迭代中被视为不同变量(§14.14.2)。

...

最后一个句子甚至明确提到了基本 for 循环变量和增强型 for 循环变量之间的差异。


5
其他的回答都很有帮助,但似乎没有直接解决问题并以清晰的术语回答问题。
在第一个示例中,您试图从 lambda 表达式中访问 k。问题在于,k 的值随时间变化(在每个循环迭代之后调用 k++)。Lambda 表达式确实会捕获外部引用,但它们需要标记为 final 或“有效地 final”(即将它们标记为 final 仍将产生有效的代码)。这是为了防止并发问题;在运行您创建的线程时,k 可能已经持有新值。
另一方面,在第二个示例中,您访问的变量是 arg,它在增强 for 循环的每次迭代中被重新初始化(与上面的示例相比,其中 k 仅被更新),因此您正在创建一个全新的变量。顺便说一下,您也可以明确声明增强 for 循环的迭代变量为 final:
for (final Integer arg : listOfInt) {
    new Thread(() -> System.out.println(arg)).start();
}

这将确保在创建的线程运行时,arg引用的值不会发生更改。


很好的澄清。 - noamt

1

增强型 for 循环被定义为等价于以下代码:

for (Iterator<T> it = iterable.iterator(); it.hasNext(); ) {
    T loopvar = it.next();
    …
}

这个替换代码解释了为什么增强型for循环的变量被认为是有效地不可变

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接