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

在Java8中,什么是集合的惯用迭代,为什么

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#中类似问题有关的以下问题:

我要提醒大家不要将C#讨论应用于Java。诚然,讨论很有趣,问题表面上也很相似。然而,Java和C#是不同的语言,因此需要考虑不同的因素

例如,提到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);