Java 8中的惯用语集合迭代

Jos*_*one 3 java java-8

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

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

要么

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

Stu*_*rks 5

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

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

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

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

在任何情况下,不要阅读C#讨论并假设类似的推理适用于Java.


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

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

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

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

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

for (String foo : foos)
    System.out.println(foo);
Run Code Online (Sandbox Code Playgroud)

这没关系.这可以改写为

foos.forEach(foo -> System.out.println(foo));
Run Code Online (Sandbox Code Playgroud)

甚至

foos.forEach(System.out::println);
Run Code Online (Sandbox Code Playgroud)

但实际上,这段代码非常简单,很难说一种方法显然更好.

在某些情况下,刻度会朝一个方向倾斜.如果循环体可以抛出一个已检查的异常,那么for循环显然会更好.如果循环体是可插入的(例如,Consumer作为参数传入)或者如果内部迭代具有不同的语义(例如,在整个调用期间锁定同步列表forEach),则新forEach方法具有边缘.

更新的例子,

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

有点复杂,但只是略微复杂.我不会用多行lambda写这个:

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

在我看来,这并没有提供直接for循环的优势,并且lambda的不同语义由第一行角落的小箭头指示.但是,(类似于Bringer128的答案)我会将这个从一个大块改写forEach为流管道:

foos.stream()
    .filter(foo -> bars.get(foo) != null)
    .forEach(System.out::println)
Run Code Online (Sandbox Code Playgroud)

我认为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");
Run Code Online (Sandbox Code Playgroud)

(为了示例,已省略了一些安全检查和注释.)

这里我们有几个嵌套的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");
Run Code Online (Sandbox Code Playgroud)

经典版本中发生的是循环控制和条件逻辑都是关于搜索数据结构的匹配.它有点扭曲,因为如果它检测到不匹配,它会在内部循环中提前中断,但如果找到匹配则从方法返回.但是一旦你长时间盯着这段代码,就可以看到它正在搜索匹配一系列标准的第一个元素,然后返回它; 如果找不到,则会抛出错误.一旦你意识到这一点,lambda/streams方法就会突然出现.它不仅缩短了很多,而且更容易理解它在做什么.

肯定有for循环会有奇怪的条件和副作用,不能轻易转换为流.但是有很多for循环只是搜索数据结构,有条件地处理元素,返回第一个匹配,或累积匹配集合,或累积变换元素.这些操作自然有助于以惯用的方式重写成流,而且我敢说.