增强的“ for”循环和lambda表达式

时间:2019-01-24 05:48:08

标签: java lambda

据我了解,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>

4 个答案:

答案 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 使其具有线程安全性。但是,在您的示例中,这很可能会返回“ before”,因为线程很可能在println执行之后执行。

答案 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循环的变量视为有效最终的