什么时候应该使用流?


99

使用a List及其stream()方法时,我刚遇到一个问题。虽然我知道如何使用它们,但是我不确定何时使用它们。

例如,我有一个列表,其中包含到不同位置的各种路径。现在,我想检查一个给定的路径是否包含列表中指定的任何路径。我想boolean根据是否满足条件返回一个。

当然,这本身并不是一项艰巨的任务。但是我想知道我应该使用流还是for(-each)循环。

名单

private static final List<String> EXCLUDE_PATHS = Arrays.asList(new String[]{
    "my/path/one",
    "my/path/two"
});

示例-流

private boolean isExcluded(String path){
    return EXCLUDE_PATHS.stream()
                        .map(String::toLowerCase)
                        .filter(path::contains)
                        .collect(Collectors.toList())
                        .size() > 0;
}

示例-每次循环

private boolean isExcluded(String path){
    for (String excludePath : EXCLUDE_PATHS) {
        if(path.contains(excludePath.toLowerCase())){
            return true;
        }
    }
    return false;
}

请注意,该path参数始终为小写

我的第一个猜测是for-each方法更快,因为如果满足条件,循环将立即返回。而该流仍将在所有列表条目上循环以完成过滤。

我的假设正确吗?如果是这样,我为什么要(或者宁愿何时使用)stream()呢?


11
流比传统的for循环更具表达力和可读性。在后面的版本中,您需要注意if-then和条件等的内在函数。流表达式非常清晰:将文件名转换为小写字母,然后按内容进行过滤,然后进行计数,收集等。结果:非常迭代计算流程的表达式。
Jean-BaptisteYunès17年

12
这里没有必要new String[]{…}。只需使用Arrays.asList("my/path/one", "my/path/two")
Holger

4
如果您的来源是a String[],则无需致电Arrays.asList。您可以使用来流式处理数组Arrays.stream(array)。顺便说一句,我很难isExcluded完全理解测试的目的。是否真正EXCLUDE_PATHS在路径中的某个位置包含的元素真的很有趣吗?即isExcluded("my/path/one/foo/bar/baz")返回true,以及isExcluded("foo/bar/baz/my/path/one/")……
霍尔格(Holger)

3
太好了,我不知道该Arrays.stream方法,感谢您指出。确实,我发布的示例对我以外的其他人似乎毫无用处。我知道该isExcluded方法的行为,但实际上这只是我自己需要的,因此,可以回答您的问题:是的,出于我不愿提及的原因,它很有趣,因为它不适合该范围原始问题。
mcuenez

1
为什么将其toLowerCase应用于已经小写的常数?它不应该应用于path论点吗?
塞巴斯蒂安·雷德尔

Answers:


78

您的假设是正确的。您的流实现比for循环慢。

但是,此流的使用应与for循环一样快:

EXCLUDE_PATHS.stream()  
                               .map(String::toLowerCase)
                               .anyMatch(path::contains);

这将遍历所有项目,String::toLowerCase逐项应用和过滤器,并终止于匹配的第一个项目

两者collect()anyMatch()是终端的操作。anyMatch()但是,第一个找到的项目退出,而collect()需要处理所有项目。


2
太棒了,不知道与findFirst()结合使用filter()。显然,我知道如何使用流。
mcuenez

4
网络上有一些关于流API性能的非常有趣的博客文章和演示,我发现这些对理解这些内容如何工作很有帮助。如果您对此感兴趣,我绝对可以建议您做一点研究。
Stefan Pries

编辑后,我觉得您的答案应该被接受,因为您也在其他答案的注释中回答了我的问题。虽然,我想为@ rvit34张贴代码提供一些功劳:-)
mcuenez

34

是否使用Streams的决定不应由性能考虑决定,而应由可读性决定。当真正涉及性能时,还有其他考虑因素。

使用您的.filter(path::contains).collect(Collectors.toList()).size() > 0方法,您正在处理所有元素并将它们收集到一个临时容器中List,然后再比较大小,但对于由两个元素组成的Stream来说,这几乎无关紧要。

.map(String::toLowerCase).anyMatch(path::contains)如果您拥有大量元素,使用可以节省CPU周期和内存。尽管如此,这会将每个String转换为其小写形式,直到找到匹配项。显然,使用有一点

private static final List<String> EXCLUDE_PATHS =
    Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
          .collect(Collectors.toList());

private boolean isExcluded(String path) {
    return EXCLUDE_PATHS.stream().anyMatch(path::contains);
}

代替。因此,您不必在每次调用时都将转换重复为小写isExcluded。如果EXCLUDE_PATHS字符串中的元素数或字符串的长度真的很大,则可以考虑使用

private static final List<Predicate<String>> EXCLUDE_PATHS =
    Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
          .map(s -> Pattern.compile(s, Pattern.LITERAL).asPredicate())
          .collect(Collectors.toList());

private boolean isExcluded(String path){
    return EXCLUDE_PATHS.stream().anyMatch(p -> p.test(path));
}

将字符串编译为带有该LITERAL标志的正则表达式模式,使其行为与普通的字符串操作相同,但是允许引擎花一些时间进行准备,例如使用Boyer Moore算法,以便在进行实际比较时更加高效。

当然,只有在有足够的后续测试来补偿准备时间时,这才有意义。除了第一个问题,确定此操作是否对性能至关重要,这是确定实际性能的一个考虑因素。并不是要使用流还是for循环的问题。

顺便说一句,上面的代码示例保留了原始代码的逻辑,这对我来说似乎值得怀疑。你的isExcluded方法返回true,如果指定的路径中包含任何在列表中的元素,所以它返回true/some/prefix/to/my/path/one,以及my/path/one/and/some/suffix甚至/some/prefix/to/my/path/one/and/some/suffix

偶数dummy/path/onerous被视为满足条件,因为它contains是字符串my/path/one...


非常感谢您对可能的性能优化的见解。关于您的答案的最后一部分:如果我对您的评论的答复不令人满意,请考虑将我的示例代码仅用作帮助他人理解我所要的内容的工具,而不是实际的代码。另外,如果您有更好的例子,也可以随时编辑问题。
mcuenez

3
我认为您确实希望执行此操作,因此无需更改它。我只留最后一部分给以后的读者,所以他们知道这不是典型的操作,而且已经进行了讨论,不需要进一步的评论……
Holger

实际上,当工作内存量超出服务器限制时,流非常适合用于内存优化
ColacX

21

是的 你是对的。您的流方法将有一些开销。但是您可以使用这样的构造:

private boolean isExcluded(String path) {
    return  EXCLUDE_PATHS.stream().map(String::toLowerCase).anyMatch(path::contains);
}

使用流的主要原因是它们使您的代码更简单易读。


3
anyMatch捷径filter(...).findFirst().isPresent()吗?
mcuenez

6
是的!这比我的第一个建议还要好。
Stefan Pries

8

Java流的目标是简化编写并行代码的复杂性。它受功能编程的启发。串行流只是为了使代码更简洁。

如果我们想要性能,我们应该使用并行流。通常,串行的速度较慢。

有一篇不错的文章可供阅读并且 ForLoopStreamParallelStream性能

在您的代码中,我们可以使用终止方法在第一个匹配项上停止搜索。(任何比赛...)


5
请注意,对于小型流和某些其他情况,并行流会由于启动成本而变慢。而且,如果您执行的是有序的终端操作,而不是无序的可并行化操作,请在最后重新同步。
CAD97

0

正如其他人提到的许多优点一样,但是我只想在流评估中提及懒惰评估。当我们确实map()创建一个小写路径流时,我们并没有立即创建整个流,而是延迟构造了该流,这就是为什么性能应等同于传统的for循环的原因。这是不是做一个完整的扫描,map()anyMatch()在同一时间执行。一旦anyMatch()返回true,它将被短路。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.