Reputation: 4448
What is considered idiomatic iteration of a Collection in Java 8, and why?
for (String foo : foos) {
String bar = bars.get(foo);
if (bar != null)
System.out.println(foo);
}
or
foos.forEach(foo -> {
String bar = bars.get(foo);
if (bar != null)
System.out.println(foo);
});
Upvotes: 3
Views: 1366
Reputation:
In general the lambda form is more idiomatic for single-statement loops, whereas the non-lambda makes more sense for multi-statement loops. (This ignores composing into a more functional style if possible).
One more style you didn't mention is the method reference:
foos.forEach(System.out::println);
EDIT:
As you're looking for a more general answer; you might find that since lambdas are new in Java, the List.forEach method is less used in practice.
In response to "So why is non-lambda more idiomatic for multi-statement?", it's more the reverse, that multi-statement lambdas are not idiomatic in most languages. Lambdas tend to be used for composition, so if I was to take the example from your question and compose it into a functional style:
// Thanks to @skiwi for fixing this code
foos.stream().filter(foo -> bars.get(foo) != null).forEach(System.out::println);
In the above example, using multi-statement lambdas would make it harder to read rather than easier.
Upvotes: 3
Reputation: 69259
You should only be using the new stream/list's forEach
if it really makes your code more concise, else stick with the old version, especially for code that gets executed linearly.
I would rewrite your statement to the following, which does make sense with streams:
foos.stream()
.filter(foo -> (bars.get(foo) != null))
.forEach(System.out::println);
This is a functional approach, that will:
List<String>
into a Stream<String>
.bars.get(foo)
is not null, which is of type Predicate<String>
.System.out::println
on the Stream<String>
, which resolves to bar -> System.out.println(bar)
, which is of type Consumer<String>
.So in more normal words:
Upvotes: 0
Reputation: 132370
In the comment thread to this answer, user Bringer128 mentioned these questions regarding a similar issue in C#:
I would caution against applying the C# discussion to Java. The discussion is interesting, to be sure, and the issues are superficially similar. However, Java and C# are different languages and thus different considerations apply.
For example, this answer mentions that the C# foreach
statement is preferable, because the compiler might be able to optimize the loop better in the future. This is not true of Java. In Java, the "enhanced for" loop is defined to be syntactic sugar for getting an Iterator
and calling its hasNext
and next
methods repeatedly. This pretty much guarantees a minimum of two method calls per loop iteration (although there is a possibility for the JIT to inline small methods).
Another example is from this answer, which mentions that in C# it is legal for the delegate invoked by a list's ForEach
method to modify the list that it's iterating. In Java there is a blanket prohibition of "interference" with the stream source for the Stream.forEach
method, whereas for the enhanced-for loop, the behavior of modifying the underlying list (or whatever) is determined by the Iterator
. Many are fail-fast and will throw ConcurrentModificationException
if the underlying list is modified during iteration. Others will silently give unexpected results.
In any case, don't read the C# discussion and assume that similar reasoning applies to Java.
Now, to answer the question. :-)
I think it's too early to declare one style to be idiomatic or preferable to another at this point. Java 8 has just been released and very few people have much experience with it. Lambdas are new and unfamiliar, and this will make many programmers uncomfortable. They'll thus want to stick to their tried-and-true for-loops. That's perfectly sensible. In a few years, though, after everyone gets used to lambdas, it might be that for-loops will start to look distinctly old-fashioned. Time will tell.
(I think this happened with generics. When they were new, they were intimidating and scary, especially wildcards. Nowadays, though, non-generic code looks distinctly old-fashioned, and to me it has a musty odor about it.)
I have an early sense of how this might turn out. Of course, I might be wrong though.
I'd say that for short loops where the computation is fixed, such as the question posted initially:
for (String foo : foos)
System.out.println(foo);
it just doesn't matter. This could be rewritten as
foos.forEach(foo -> System.out.println(foo));
or even
foos.forEach(System.out::println);
But really, this code is so simple that it's hard to argue that one way is clearly better.
There are situations where the scales tip in one direction or another. If the loop body can throw a checked exception, a for-loop is clearly better. If the loop body is pluggable (e.g., the Consumer
is passed in as a parameter) or if internal iteration has different semantics (e.g., locking of a synchronized list during the entire call to forEach
) then the new forEach
approach has the edge.
The updated example,
for (String foo : foos) {
String bar = bars.get(foo);
if (bar != null)
System.out.println(foo);
}
is a bit more complicated, but only slightly. I would not write this using a multi-line lambda:
foos.forEach(foo -> {
String bar = bars.get(foo);
if (bar != null)
System.out.println(foo);
});
This offers no advantage over the straight for-loop, in my opinion, and the different semantics of the lambda are signaled by the little arrow way up in the corner of the first line. However, (similar to Bringer128's answer) I would recast this from a big forEach
block into a stream pipeline:
foos.stream()
.filter(foo -> bars.get(foo) != null)
.forEach(System.out::println)
I think the lambda/streams approach starts to show a bit of an advantage here, but only a bit, as this is still a really simple example. Using lambda/streams replaces some conditional control logic with a data filtering operation. This might make sense for some operations, but not for others.
The difference between the approaches starts to become clearer as things get more complicated. The simple examples are so simple that it's obvious what they do. Real-world examples can be considerably more complex. Consider this code from the method Class.getEnclosingMethod of the JDK (scroll to lines 1023-1052):
Class<?> enclosingCandidate = enclosingInfo.getEnclosingClass();
// ...
for(Method m: enclosingCandidate.getDeclaredMethods()) {
if (m.getName().equals(enclosingInfo.getName()) ) {
Class<?>[] candidateParamClasses = m.getParameterTypes();
if (candidateParamClasses.length == parameterClasses.length) {
boolean matches = true;
for(int i = 0; i < candidateParamClasses.length; i++) {
if (!candidateParamClasses[i].equals(parameterClasses[i])) {
matches = false;
break;
}
}
if (matches) { // finally, check return type
if (m.getReturnType().equals(returnType) )
return m;
}
}
}
}
throw new InternalError("Enclosing method not found");
(Some security checks and comments have been omitted for the sake of the example.)
Here we have a couple nested for-loops with a couple levels of conditional logic and a boolean flag. Read through this code for a while and see if you can figure out what it does.
Using lambda and streams, this code can be rewritten as follows:
return Arrays.stream(enclosingInfo.getEnclosingClass().getDeclaredMethods())
.filter(m -> Objects.equals(m.getName(), enclosingInfo.getName()))
.filter(m -> Arrays.equals(m.getParameterTypes(), parameterClasses))
.filter(m -> Objects.equals(m.getReturnType(), returnType))
.findFirst()
.orElseThrow(() -> new InternalError("Enclosing method not found");
What's going on in the classic version is that the loop control and conditional logic is all about searching a data structure for a match. It's a bit contorted because it breaks early out of the inner loop if it detects a non-match, but returns early from the method if it does find a match. But once you stare at this code long enough, you can see that it's searching for the first element that matches a series of criteria, and returns it; and if it doesn't find one, it throws an error. Once you realize that, the lambda/streams approach just pops right out. Not only is it a lot shorter, it's much easier to understand what it's doing.
There are certainly for-loops that will have weird conditions and side effects that can't be turned easily into streams. But there are a lot of for-loops that are just searching data structures, processing elements conditionally, returning the first match, or accumulating a collection of matches, or accumulating transformed elements. These operations naturally lend themselves to being rewritten into streams, and dare I say, in an idiomatic fashion.
Upvotes: 4