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

2022-09-01 03:48:51

根据我的理解,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'
 }

为什么它对于增强型循环而不是普通常规循环工作正常,尽管增强型循环也在内部的某个地方,像普通循环一样递增变量。forforfor


答案 1

Lambda 表达式的工作方式类似于回调。在代码中传递它们的那一刻,它们“存储”它们操作所需的任何外部值(或引用)(就好像这些值在函数调用中作为参数传递一样)。这只是对开发人员隐藏的)。在第一个示例中,您可以通过存储到单独的变量(如 d)来解决此问题:k

for (int k = 0; k < 10; k++) {
    final int d = k
    new Thread(() -> System.out.println(d)).start();
}

实际上意味着,在上面的示例中,您可以省略“final”关键字,因为它实际上是final,因为它在其范围内永远不会更改。finald

for环路以不同的方式运行。它们是迭代代码(而不是回调)。它们在各自的范围内工作,并且可以在自己的堆栈上使用所有变量。这意味着,循环的代码块是外部代码块的一部分。for

至于你强调的问题:

增强型循环不与常规索引计数器一起运行,至少不能直接运行。增强的循环(在非数组上)创建一个隐藏的迭代器。您可以通过以下方式对此进行测试:forfor

Collection<String> mySet = new HashSet<>();
mySet.addAll(Arrays.asList("A", "B", "C"));
for (String myString : mySet) {
    if (myString.equals("B")) {
        mySet.remove(myString);
    }
}

上面的示例将导致并发模式异常。这是由于迭代器注意到基础集合在执行期间已更改。但是,在您的示例中,外部循环创建了一个可以在 lambda 表达式中引用的“有效最终”变量,因为该值是在执行时捕获的。arg

防止捕获“非有效最终”值或多或少只是Java中的一种预防措施,因为在其他语言(例如JavaScript)中,这的工作方式不同。

因此,编译器理论上可以转换您的代码,捕获该值并继续,但它必须以不同的方式存储该值,并且您可能会得到意外的结果。因此,为 Java 8 开发 lambda 的团队通过阻止这种情况(有异常)正确地排除了这种情况。

如果您需要更改 lambda 表达式中外部变量的值,则可以声明一个单元素数组:

String[] myStringRef = { "before" };
someCallingMethod(() -> myStringRef[0] = "after" );
System.out.println(myStringRef[0]);

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


答案 2

在增强型 for 循环中,每次迭代都会初始化变量。来自 Java 语言规范 (JLS) 的 §14.14.2

...

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

  • 如果表达式的类型是 的子类型,则翻译如下所示。Iterable

    如果表达式的类型是某个类型参数 的子类型,则设为类型 ;否则,设为 原始类型 。Iterable<X>XIjava.util.Iterator<X>Ijava.util.Iterator

    增强语句等效于以下形式的基本语句:forfor

    for (I #i = Expression.iterator(); #i.hasNext(); ) {
        {VariableModifier} TargetType Identifier =
            (TargetType) #i.next();
        Statement
    }
    

...

  • 否则,表达式必须具有数组类型 。T[]

    设为紧挨在增强语句之前的标签序列(可能为空)。L1 ... Lmfor

    增强语句等效于以下形式的基本语句:forfor

    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 循环中的变量(在您的情况下)初始化一次,并在每次迭代时更新(如果存在“ForUpdate”,例如 )。有关详细信息,请参阅 JLS 的 §14.14.1。由于变量已更新,因此每次迭代都不是最终的,也不是有效的最终迭代。kk++

JLS 的 §15.27.2 强制要求并解释了对最终变量或有效最终变量的需求:

...

在 lambda 表达式中使用但未声明的任何局部变量、形式参数或异常参数都必须声明为 final 或实际上是 final (§4.12.4),否则在尝试使用时会发生编译时错误。

在 lambda 主体中使用但未声明的任何局部变量都必须在 lambda 主体之前明确赋值 (§16(定赋值)),否则会发生编译时错误。

关于变量使用的类似规则也适用于内部类的主体 (§8.1.3)。对有效最终变量的限制禁止访问动态变化的局部变量,这些局部变量的捕获可能会引入并发问题。与最终的限制相比,它减轻了程序员的文书负担。

对有效最终变量的限制包括标准循环变量,但不包括增强型循环变量,对于循环的每次迭代,这些变量被视为不同 (§14.14.2)。

...

最后一句话甚至明确提到了基本for循环变量和增强的for循环变量之间的区别。


推荐