Java8中的惯用集合迭代
在Java8中,什么是集合的惯用迭代,为什么Java8中的惯用集合迭代,java,java-8,Java,Java 8,在Java8中,什么是集合的惯用迭代,为什么 for (String foo : foos) { String bar = bars.get(foo); if (bar != null) System.out.println(foo); } 或 通常,lambda形式对于单语句循环更为惯用,而非lambda形式对于多语句循环更有意义。(如果可能的话,这会忽略组合成更具功能性的样式) 您没有提到的另一个样式是方法引用: foos.forEach(System.out::print
for (String foo : foos) {
String bar = bars.get(foo);
if (bar != null)
System.out.println(foo);
}
或
通常,lambda形式对于单语句循环更为惯用,而非lambda形式对于多语句循环更有意义。(如果可能的话,这会忽略组合成更具功能性的样式) 您没有提到的另一个样式是方法引用:
foos.forEach(System.out::println);
编辑:
当你在寻找一个更一般的答案时;您可能会发现,由于lambda在Java中是新的,因此在实践中很少使用List.forEach方法
在回答“为什么非lambda对于多语句更为惯用?”时,情况恰恰相反,多语句lambda在大多数语言中都不是惯用的。Lambdas通常用于合成,因此,如果我从您的问题中选取一个示例,并将其合成为功能性风格:
// Thanks to @skiwi for fixing this code
foos.stream().filter(foo -> bars.get(foo) != null).forEach(System.out::println);
在上面的例子中,使用多语句lambda会使阅读变得更难,而不是更容易。在用户的评论线程中,用户提到了与C#中类似问题有关的以下问题:
foreach
语句更可取,因为编译器将来可能能够更好地优化循环。Java不是这样。在Java中,“enhanced for”循环被定义为语法糖,用于获取迭代器
,并重复调用其hasNext
和next
方法。这几乎保证了每个循环迭代至少有两个方法调用(尽管JIT有可能内联小方法)
另一个例子是from,它提到在C#中,列表的ForEach
方法调用的委托修改它正在迭代的列表是合法的。在Java中,stream.forEach
方法全面禁止“干扰”流源,而对于增强的for循环,修改底层列表(或任何内容)的行为由迭代器决定。如果在迭代过程中修改了基础列表,许多都会快速失败并抛出ConcurrentModificationException
。其他人会默默地给出意想不到的结果
在任何情况下,不要阅读C#讨论,并假设类似的推理适用于Java
现在,回答这个问题。:-)
我认为现在宣布一种风格是惯用的或比另一种更可取还为时过早。Java8刚刚发布,很少有人对它有丰富的经验。lambda是新的和不熟悉的,这会让许多程序员感到不舒服。因此,他们将希望坚持他们的尝试和真实的for循环。这是非常明智的。不过,在几年后,当每个人都习惯了lambdas之后,for循环可能会开始显得明显过时。时间会证明一切
(我认为这发生在泛型上。当它们是新的时候,它们是可怕的,尤其是通配符。不过,现在,非泛型代码看起来明显过时,在我看来,它有一种发霉的气味。)
我很早就意识到这可能会变成什么样子。当然,我可能错了
我认为对于计算固定的短循环,例如最初发布的问题:
for (String foo : foos)
System.out.println(foo);
没关系。这可以改写为
foos.forEach(foo -> System.out.println(foo));
甚至
foos.forEach(System.out::println);
但实际上,这段代码非常简单,很难说一种方法显然更好
有些情况下,天平会朝一个或另一个方向倾斜。如果循环体可以抛出选中的异常,那么for循环显然更好。如果循环体是可插入的(例如,使用者
作为参数传入),或者如果内部迭代具有不同的语义(例如,在整个调用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的不同语义由第一行拐角处向上的小箭头表示。但是,(类似于)我会将它从一个大的forEach
块重新转换为一个流管道:
foos.stream()
.filter(foo -> bars.get(foo) != null)
.forEach(System.out::println)
我认为lambda/streams方法在这里开始显示出一点优势,但只有一点,因为这仍然是一个非常简单的示例。使用lambda/streams将某些条件控制逻辑替换为数据过滤操作。这可能对某些操作有意义,但对其他操作没有意义
随着事情变得越来越复杂,两种方法之间的区别开始变得越来越明显。这些简单的例子非常简单,它们的作用显而易见。现实世界的例子可能要复杂得多。从JDK(滚动到1023-1052)的方法考虑这个代码:
在经典版本中,循环控制和条件逻辑都是关于在数据结构中搜索匹配项的。它有点扭曲,因为如果检测到不匹配,它会提前从内部循环中断,但是如果找到匹配,它会提前从方法返回。但是,一旦你盯着这段代码看了足够长的时间,你就会发现它正在搜索匹配一系列条件的第一个元素,并返回它;如果找不到,就会抛出一个错误。一旦您意识到这一点,lambda/streams方法就会立即出现。它不仅要短得多,而且更容易理解它在做什么
当然也有一些循环会产生奇怪的情况和副作用,这些都是不容易改变的
foos.stream()
.filter(foo -> bars.get(foo) != null)
.forEach(System.out::println)
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");
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");
foos.stream()
.filter(foo -> (bars.get(foo) != null))
.forEach(System.out::println);