The lambda expressions introduced in Java 8 can capture, or “close over”, any local variable that’s within scope and effectively final (Java 8 Language Specification §15.27.2, §4.12.4). Interestingly, this includes the loop variables of enhanced for
loops, or for-each loops as I prefer to call them. Cay Horstmann mentions this very useful but non-obvious fact on page 119 of his new book Core Java for the Impatient.
Why is the loop variable of a for-each loop effectively final? Doesn’t it get updated in each iteration, just like its equivalent in an ordinary for
loop? No, because the Java 8 Language Specification explicitly requires (§14.14.2) that the for-each loop variable is declared anew within the loop body for every single iteration, and only once assigned a value: that of the current array element or iterator result. Therefore the loop variable is effectively final in each iteration.
This has two important consequences. The first is that yes, you can directly use the for-each loop variable in any lambda expression you care to define within the loop. The second is that this variable will actually provide the value you would expect it to provide, namely the value during the iteration when the lambda expression was being evaluated for later execution. If you’re a C# programmer you will immediately understand why this is noteworthy. Otherwise, read on to learn why being unable to close over mutable variables (including for
loop variables) is actually a good thing…
Java Loop Closures
First, a very simple example to illustrate what we’re talking about here. The following program stores three Runnable
lambdas for later execution. Each captures one of the strings in an array that our for-each loop iterates over. The output is exactly what you’d expect: a b c
.
import java.util.ArrayList;
public class ClosureTest {
public static void main(String[] args) {
final String[] items = new String[] { "a", "b", "c" };
final ArrayList<Runnable> lambdas = new ArrayList<>();
for (String item: items)
lambdas.add(() -> System.out.println(item));
lambdas.forEach(Runnable::run);
}
}
C# Loop Closures
The current C# version, C# 5 shipping with .NET 4.5, defines the foreach
loop variable in the same fashion as Java 8 and produces identical lambda capturing behavior. However, that was not always the case. Prior to C# 5, foreach
loop variables were living and mutable through all loop iterations, just like ordinary for
loop variables.
This resulted in the vexing behavior Eric Lippert describes in Closing over the loop variable considered harmful. When lambdas were defined during a loop but invoked at a later time, they would all see the final value of the loop variable, rather than the current one during the iteration when they were evaluated! The only way to avoid this was to manually copy the current iteration value into a newly defined variable, and pass that variable to the lambda expression.
If you’re running Windows you can easily try this out for yourself, without having to install Visual Studio or other tools. Just save the following test program to file ClosureTest.cs
or the like, then compile it using either of the two compiler versions listed below:
using System;
using System.Collections.Generic;
public static class ClosureTest {
public static void Main() {
var items = new String[] { "a", "b", "c" };
var lambdas = new List<Action>();
foreach (String item in items)
lambdas.Add(() => Console.WriteLine(item));
foreach (Action lambda in lambdas)
lambda();
}
}
- Compiled with
C:\Windows\Microsoft.NET\Framework64\<b>v4.0.30319</b>\csc.exe
, the output isa b c
. - Compiled with
C:\Windows\Microsoft.NET\Framework64\<b>v3.5</b>\csc.exe
, the output isc c c
!
Note that the v4.0.30319
directory actually contains .NET 4.5 with C# 5 on my system. This was an in-place upgrade for .NET 4.0 and so reuses the same directory structure, including the old version name. Interestingly, specifying the /langversion
switch on the new compiler in order to emulate C# 3/4 does not change the foreach
capturing behavior. That strikes me as a bug in the implementation of langversion
but I suppose there’s little demand for a behavior that nobody wanted in the first place…
You’ll probably agree that the foreach
capturing of older C# versions is extremely counter-intuitive, but note that it still necessarily persists in for
loops. Java avoids the possibility of such behavior entirely by requiring that all captured local variables are effectively final. While sometimes restrictive, it’s easy enough to work around when you do need a mutable value (just use a class-scoped variable) and saves thousands of programmers from nerve-wracking debugging sessions.