猿问

增强的“for”循环和 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循环工作正常,尽管增强for循环也在内部某处增加变量,如普通循环所做的那样。**


料青山看我应如是
浏览 354回答 3
3回答

翻过高山走不出你

Lambda 表达式的工作方式类似于回调。在将它们传递到代码中的那一刻,它们“存储”了它们操作所需的任何外部值(或引用)(就好像这些值在函数调用中作为参数传递一样。这只是对开发人员隐藏)。k在您的第一个示例中,您可以通过存储到单独的变量(如 d)来解决此问题:for (int k = 0; k < 10; k++) {&nbsp; &nbsp; final int d = k&nbsp; &nbsp; 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) {&nbsp; &nbsp; if (myString.equals("B")) {&nbsp; &nbsp; &nbsp; &nbsp; mySet.remove(myString);&nbsp; &nbsp; }}上面的示例将导致 ConcurrentModificationException。这是由于迭代器注意到底层集合在执行期间发生了变化。但是,在您的示例中,外部循环创建了一个“有效的最终”变量arg,可以在 lambda 表达式中引用,因为该值是在执行时捕获的。防止捕获“非有效最终”值或多或少只是 Java 中的一种预防措施,因为在其他语言(例如 JavaScript)中,这会有所不同。所以编译器理论上可以翻译你的代码,捕获值,然后继续,但它必须以不同的方式存储那个值,你可能会得到意想不到的结果。因此,为 Java 8 开发 lambdas 的团队正确地排除了这种情况,通过异常阻止它。如果您需要更改 lambda 表达式中的外部变量的值,您可以声明一个单元素数组:String[] myStringRef = { "before" };someCallingMethod(() -> myStringRef[0] = "after" );System.out.println(myStringRef[0]);或使用AtomicReference<T>使其成为线程安全的。但是,对于您的示例,这可能会返回“之前”,因为回调很可能在 println 执行之后执行。

慕工程0101907

在增强的 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(); ) {&nbsp; &nbsp; {VariableModifier} TargetType Identifier =&nbsp; &nbsp; &nbsp; &nbsp; (TargetType) #i.next();&nbsp; &nbsp; Statement}...否则,表达式必须具有数组类型,T[]。让L1 ... Lm是紧接在增强for语句之前的(可能为空的)标签序列。增强for语句等价于for以下形式的基本语句:T[] #a = Expression;L1: L2: ... Lm:for (int #i = 0; #i < #a.length; #i++) {&nbsp; &nbsp; {VariableModifier} TargetType Identifier = #a[#i];&nbsp; &nbsp; Statement}...换句话说,您的增强 for 循环等效于:ArrayList<Integer> listOfInt = new ArrayList<>();// add elements...for (Iterator<Integer> itr = listOfInt.iterator(); itr.hasNext(); ) {&nbsp; &nbsp; Integer arg = itr.next();&nbsp; &nbsp; 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 循环变量之间的区别。

慕容森

其他回复很有帮助,但他们似乎没有直接解决问题并明确回答。在您的第一个示例中,您尝试k从 lambda 表达式进行访问。这里的问题是k随着时间的推移改变它的值(k++在每次循环迭代之后调用)。Lambda 表达式确实捕获了外部引用,但它们需要被标记为final或“有效地最终”(即,将它们标记为final仍会产生有效代码)。这是为了防止并发问题;在您创建的线程运行时,k可能已经拥有一个新值。另一方面,在您的第二个示例中,您正在访问的变量是arg,它会在增强的 for 循环的每次迭代中重新初始化(与上面的示例相比,k仅更新),因此您正在创建一个完全每次迭代的新变量。顺便说一句,您还可以将增强型 for 循环的迭代变量显式声明为final:for (final Integer arg : listOfInt) {&nbsp; &nbsp; new Thread(() -> System.out.println(arg)).start();}这可确保在arg您创建的线程运行时值引用不会更改。
随时随地看视频慕课网APP

相关分类

Java
我要回答