在this answer 的评论线程中,用户Bringer128 提到了有关C# 中类似问题的这些问题:
我会告诫不要将 C# 讨论应用于 Java。可以肯定的是,讨论很有趣,而且问题表面上相似。但是,Java 和 C# 是不同的语言,因此需要考虑不同的因素。
例如,this answer 提到 C# foreach 语句更可取,因为编译器将来可能能够更好地优化循环。 Java 并非如此。在 Java 中,“增强的 for”循环被定义为语法糖,用于获取 Iterator 并重复调用其 hasNext 和 next 方法。这几乎可以保证每次循环迭代至少调用两个方法(尽管 JIT 有可能内联小方法)。
另一个例子来自this answer,它提到在C# 中,由列表的ForEach 方法调用的委托修改它正在迭代的列表是合法的。在 Java 中,Stream.forEach 方法全面禁止“干扰”流源,而对于增强型 for 循环,修改底层列表(或其他)的行为由 Iterator 确定。许多都是快速失败的,如果在迭代期间修改了基础列表,则会抛出ConcurrentModificationException。别人会默默地给出意想不到的结果。
无论如何,不要阅读 C# 讨论并假设类似的推理适用于 Java。
现在,回答这个问题。 :-)
我认为现在宣布一种风格是惯用的或优于另一种风格还为时过早。 Java 8 刚刚发布,很少有人对此有丰富的经验。 Lambda 是新的和陌生的,这会让许多程序员感到不舒服。因此,他们会想要坚持他们久经考验的 for 循环。这是完全明智的。不过,几年后,在每个人都习惯了 lambda 之后,for 循环可能会开始显得明显过时。时间会证明一切。
(我认为这发生在泛型上。当它们是新的时,它们令人生畏和可怕,尤其是通配符。不过,如今,非泛型代码看起来明显过时了,而且对我来说它有一股霉味。 )
我很早就知道这可能会变成什么样子。当然,我可能错了。
我会说对于计算固定的短循环,例如最初发布的问题:
for (String foo : foos)
System.out.println(foo);
没关系。这可以重写为
foos.forEach(foo -> System.out.println(foo));
甚至
foos.forEach(System.out::println);
但实际上,这段代码非常简单,很难说一种方法明显更好。
在某些情况下,天平会朝一个方向或另一个方向倾斜。如果循环体可以抛出检查异常,那么 for 循环显然更好。如果循环体是可插入的(例如,Consumer 作为参数传入)或者如果内部迭代具有不同的语义(例如,在对 forEach 的整个调用期间锁定同步列表),那么新的 forEach方法有优势。
更新的示例,
for (String foo : foos) {
String bar = bars.get(foo);
if (bar != null)
System.out.println(foo);
}
有点复杂,但只是稍微复杂一点。我会不使用多行 lambda 编写此代码:
foos.forEach(foo -> {
String bar = bars.get(foo);
if (bar != null)
System.out.println(foo);
});
在我看来,这与直接的 for 循环相比没有任何优势,并且 lambda 的不同语义由第一行角上的小箭头表示。但是,(类似于Bringer128's answer)我会将它从一个大的forEach 块重新转换为流管道:
foos.stream()
.filter(foo -> bars.get(foo) != null)
.forEach(System.out::println)
我认为 lambda/streams 方法在这里开始显示出一点优势,但只是一点点,因为这仍然是一个非常简单的示例。使用 lambda/streams 将一些条件控制逻辑替换为数据过滤操作。这可能对某些操作有意义,但对其他操作没有意义。
随着事情变得越来越复杂,这两种方法之间的区别开始变得更加清晰。简单的例子非常简单,很明显它们做了什么。现实世界的例子可能要复杂得多。考虑 JDK 的方法 Class.getEnclosingMethod 中的这段代码(滚动到第 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");
(为了示例,省略了一些安全检查和 cmets。)
这里我们有几个嵌套的 for 循环,带有几个级别的条件逻辑和一个布尔标志。阅读这段代码一段时间,看看你是否能弄清楚它的作用。
使用 lambda 和流,这段代码可以重写如下:
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");
经典版本中发生的事情是循环控制和条件逻辑都是关于在数据结构中搜索匹配项。它有点扭曲,因为如果它检测到不匹配,它会提前退出内部循环,但如果它确实找到匹配,则提前从方法返回。但是一旦你盯着这段代码足够长的时间,你会发现它正在搜索第一个匹配一系列条件的元素,并返回它;如果找不到,则会引发错误。一旦你意识到这一点,lambda/streams 方法就会立即出现。它不仅更短,而且更容易理解它在做什么。
肯定有 for 循环会产生奇怪的条件和副作用,不能轻易地转化为流。但是有很多 for 循环只是搜索数据结构,有条件地处理元素,返回第一个匹配项,或者累积匹配的集合,或者累积转换后的元素。这些操作很自然地适合被重写为流,我敢说,以一种惯用的方式。