Java 8中的惯用语集合迭代

时间:2014-04-02 01:52:10

标签: java java-8

什么被认为是Java 8中Collection的惯用迭代,为什么?

for (String foo : foos) {
  String bar = bars.get(foo);
  if (bar != null)
    System.out.println(foo);
}

foos.forEach(foo -> {
  String bar = bars.get(foo);
  if (bar != null)
    System.out.println(foo);
});

3 个答案:

答案 0 :(得分:5)

this answer的评论主题中,用户Bringer128提到了有关C#中类似问题的这些问题:

我会警告不要将C#讨论应用于Java。可以肯定的是,讨论很有趣,而且这些问题在表面上是相似的。但是,Java和C#是不同的语言,因此适用不同的考虑因素。

例如,this answer提到C#foreach语句更可取,因为编译器可能会在将来更好地优化循环。 Java不是这样。在Java中,"增强了"循环被定义为获取Iterator并重复调用其hasNextnext方法的语法糖。这几乎保证了每次循环迭代最少两次方法调用(尽管JIT有可能内联小方法)。

另一个例子来自this answer,它提到在C#中,由列表的ForEach方法调用的委托修改它迭代的列表是合法的。在Java中,全面禁止干扰"使用Stream.forEach方法的流源,而对于增强型for循环,修改基础列表(或其他)的行为由Iterator确定。如果在迭代期间修改了基础列表,则许多都会快速失败并抛出ConcurrentModificationException。其他人会默默地给出意想不到的结果。

无论如何,请不要阅读C#讨论并假设类似的推理适用于Java。


现在,回答这个问题。 : - )

我认为现在宣布一种风格是惯用的或者在另一种风格上优于另一种风格为时尚早。 Java 8刚刚发布,很少有人有这方面的经验。 Lambdas是新的,不熟悉的,这将使许多程序员感到不舒服。因此,他们希望坚持他们久经考验的for循环。这是非常明智的。但是,在几年之后,每个人都习惯了lambdas之后,可能会出现for-loops开始显得过时的老式。时间会证明。

(我认为这发生在仿制药上。当它们是新的时,它们是令人生畏和可怕的,尤其是通配符。但是,现在,非通用代码看起来非常老式,对我来说它有一种霉味。 )

我很早就意识到这可能会如何发展。当然,我可能错了。

我说对于修正计算的短循环,例如最初发布的问题:

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");

(为了举例,已经省略了一些安全检查和注释。)

这里我们有几个嵌套的for循环,有几个级别的条件逻辑和一个布尔标志。仔细阅读这段代码,看看你能否弄清楚它的作用。

使用lambda和stream,可以按如下方式重写此代码:

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循环只是搜索数据结构,有条件地处理元素,返回第一个匹配,或累积匹配集合,或累积变换元素。这些操作自然有助于以惯用的方式重写成流,而且我敢说。

答案 1 :(得分:3)

通常,lambda形式对于单语句循环更为惯用,而非lambda对于多语句循环更有意义。 (如果可能的话,这会忽略编写成更具功能性的风格。)

你没有提到的另一种风格是方法参考:

foos.forEach(System.out::println);

编辑:

正在寻找更一般的答案;你可能会发现,因为lambda是Java中的新东西,所以List.forEach方法在实践中的使用较少。

为了回应“那么为什么非语言对于多语句更加惯用?”,更多的是相反,多语句lambda在大多数语言中都不是惯用语。 Lambdas倾向于用于合成,所以如果我从你的问题中拿出例子并将其组合成一个功能样式:

// Thanks to @skiwi for fixing this code
foos.stream().filter(foo -> bars.get(foo) != null).forEach(System.out::println);

在上面的例子中,使用多语句lambdas会使阅读变得更难而不是更容易。

答案 2 :(得分:1)

你应该只使用新的流/列表forEach,如果它真的让你的代码更简洁,否则坚持旧版本,特别是对于线性执行的代码。

我会将您的语句重写为以下内容, 对流有意义:

foos.stream()
        .filter(foo -> (bars.get(foo) != null))
        .forEach(System.out::println);

这是一种功能性方法,将:

  1. 将您的List<String>变为Stream<String>
  2. 过滤对象,以便保留bars.get(foo)不为空的所有元素,类型为Predicate<String>
  3. 然后,您致电System.out::println上的Stream<String>,该bar -> System.out.println(bar)结算为Consumer<String> {{1}}。
  4. 所以更正常的话:

    1. 获取流。
    2. 过滤掉所有不需要的元素,保留想要的元素。
    3. 使用流中的所有元素。